├── .gitattributes
├── .github
└── workflows
│ ├── pages.yml
│ └── test.yml
├── .gitignore
├── .goreleaser.yaml
├── LICENSE
├── Makefile
├── README.md
├── client
├── .gitignore
├── .prettierrc
├── .vscode
│ └── extensions.json
├── README.md
├── index.html
├── jest.config.js
├── jest.setup.ts
├── package.json
├── roots
│ └── mermaid.html
├── src
│ ├── App.svelte
│ ├── Mermaid.svelte
│ ├── app.css
│ ├── components
│ │ ├── DetailsCard.svelte
│ │ ├── ExaminationResultTable.svelte
│ │ ├── ExplainText.svelte
│ │ ├── ExplainTree
│ │ │ ├── ExplainTree.ts
│ │ │ └── ExplainTreeChart.ts
│ │ ├── IndexSuggestion.svelte
│ │ └── PlainText.svelte
│ ├── contexts
│ │ └── HighlightIndexContext.ts
│ ├── dummies
│ │ ├── DummyDigData.ts
│ │ ├── DummySuggestData.ts
│ │ ├── analyzeHasuraPostgresNodes.json
│ │ ├── analyzeMysqlNodes.json
│ │ ├── analyzePostgresNodes.json
│ │ ├── digSqlDbLoads.json
│ │ ├── digTokenizedSqlDbLoads.json
│ │ ├── hasuraPostgresQuery.sql
│ │ └── jaegerTraces.json
│ ├── dummy.ts
│ ├── lib
│ │ ├── GrChart.ts
│ │ └── components
│ │ │ └── CommandCode.svelte
│ ├── main.ts
│ ├── mermaid.ts
│ ├── models
│ │ ├── BaseSuggestData.ts
│ │ ├── DbLoad.ts
│ │ ├── DbLoadOfSql.ts
│ │ ├── DigData.ts
│ │ ├── ExaminationCommandOption.ts
│ │ ├── ExaminationIndexResult.ts
│ │ ├── ExaminationResult.ts
│ │ ├── HasuraIndexTarget.ts
│ │ ├── HasuraSuggestData.ts
│ │ ├── IndexColumn.ts
│ │ ├── IndexTarget.ts
│ │ ├── JaegerData.ts
│ │ ├── MysqlSuggestData.ts
│ │ ├── OtelCompactedTrace.ts
│ │ ├── OtelTraceSpan.ts
│ │ ├── PerformanceInsightsData.ts
│ │ ├── PostgresIndexTarget.ts
│ │ ├── PostgresSuggestData.ts
│ │ ├── SuggestData.ts
│ │ ├── TimeDbLoad.ts
│ │ ├── explain_data
│ │ │ ├── DbAnalyzeData.ts
│ │ │ ├── DbExplainData.ts
│ │ │ ├── MysqlAnalyzeData.ts
│ │ │ ├── PostgresAnalyzeData.ts
│ │ │ └── PostgresExplainData.ts
│ │ └── trace_diagram
│ │ │ ├── ITraceNode.ts
│ │ │ ├── TraceNode.ts
│ │ │ ├── TraceRepeatNode.ts
│ │ │ └── TraceTree.ts
│ ├── pages
│ │ ├── dig
│ │ │ ├── jaeger
│ │ │ │ ├── DigJaegerPage.svelte
│ │ │ │ ├── TraceDiagram.svelte
│ │ │ │ └── TracesTable.svelte
│ │ │ └── performance_insights
│ │ │ │ ├── DbLoadChart.ts
│ │ │ │ ├── DbLoadRankingChart.ts
│ │ │ │ ├── DbTokenizedLoadChart.ts
│ │ │ │ ├── DbTokenizedLoadRankingChart.ts
│ │ │ │ ├── DigPerformanceInsightsPage.svelte
│ │ │ │ ├── HeaviestSqlRanking.svelte
│ │ │ │ ├── HeaviestTokenizedSqlRanking.svelte
│ │ │ │ ├── HeavySqlTimeline.svelte
│ │ │ │ ├── HeavyTokenizedSqlTimeline.svelte
│ │ │ │ └── util
│ │ │ │ ├── CommandText.ts
│ │ │ │ ├── Databases.ts
│ │ │ │ ├── LoadTooltip.ts
│ │ │ │ └── chart
│ │ │ │ ├── LoadPropChartSereies.ts
│ │ │ │ └── TokenizedLoad.ts
│ │ └── suggest
│ │ │ ├── hasura
│ │ │ ├── ExaminationLineMenu.svelte
│ │ │ ├── ExplainChart.svelte
│ │ │ └── SuggestHasuraPage.svelte
│ │ │ ├── mysql
│ │ │ ├── ExplainAnalyzeChart.svelte
│ │ │ └── SuggestMysqlPage.svelte
│ │ │ └── postgres
│ │ │ ├── ExplainAnalyzeChart.svelte
│ │ │ └── SuggestPostgresPage.svelte
│ ├── stores
│ │ └── gr_param.ts
│ ├── types
│ │ ├── apexcharts.d.ts
│ │ ├── gr_param.d.ts
│ │ ├── svelte-chota.d.ts
│ │ └── window.d.ts
│ └── vite-env.d.ts
├── svelte.config.js
├── test
│ └── src
│ │ ├── lib
│ │ └── ExplainAnalyzeTree.test.ts
│ │ └── pages
│ │ ├── dig
│ │ ├── jaeger
│ │ │ └── DigJaegerPage.test.ts
│ │ └── performance_insights
│ │ │ └── DigPerformanceInsightsPage.test.ts
│ │ └── suggest
│ │ ├── hasura
│ │ └── SuggestHasuraPage.test.ts
│ │ ├── mysql
│ │ └── SuggestMysqlPage.test.ts
│ │ └── postgres
│ │ └── SuggestPostgresPage.test.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite-common.config.ts
├── vite-main.config.ts
├── vite-mermaid.config.ts
├── vite.config.ts
└── yarn.lock
├── cmd
├── app.go
├── app
│ ├── dig.go
│ └── dig
│ │ ├── jaeger.go
│ │ └── jaeger_test.go
├── db.go
├── db
│ ├── dig.go
│ ├── dig
│ │ ├── performance_insights.go
│ │ └── performance_insights_test.go
│ ├── suggest.go
│ └── suggest
│ │ ├── hasura.go
│ │ ├── hasura
│ │ ├── postgres.go
│ │ └── postgres_test.go
│ │ ├── mysql.go
│ │ ├── mysql_test.go
│ │ ├── postgres.go
│ │ └── postgres_test.go
├── flag
│ ├── app_flag.go
│ ├── db_flag.go
│ └── global_flag.go
├── root.go
├── util
│ └── log.go
└── version.go
├── database
├── dmodel
│ ├── examination_index_result.go
│ ├── examination_result.go
│ ├── index_column.go
│ ├── index_target.go
│ ├── index_target_test.go
│ └── single_table_explain_result.go
├── dservice
│ ├── build_single_table_explain_results.go
│ ├── build_single_table_explain_results_test.go
│ ├── dparser
│ │ ├── build_table_fields.go
│ │ ├── column_schema.go
│ │ ├── field.go
│ │ ├── field_column.go
│ │ ├── field_type.go
│ │ ├── index_field.go
│ │ ├── index_target_builder.go
│ │ ├── index_target_builder_test.go
│ │ ├── index_target_table.go
│ │ ├── stmt_scope.go
│ │ ├── table.go
│ │ └── table_schema.go
│ ├── examiner.go
│ └── existing_index_remover.go
├── hasura
│ └── hservice
│ │ ├── index_examiner.go
│ │ ├── index_getter.go
│ │ ├── index_suggester.go
│ │ └── parser
│ │ ├── collect_table_schemas.go
│ │ └── index.go
├── mysql
│ ├── mmodel
│ │ ├── analyze_result_line.go
│ │ ├── explain_analyze_result_line.go
│ │ ├── explain_analyze_result_line_test.go
│ │ ├── explain_analyze_tree.go
│ │ └── explain_analyze_tree_node.go
│ └── mservice
│ │ ├── explainer.go
│ │ ├── explainer_test.go
│ │ ├── index_examiner.go
│ │ ├── index_getter.go
│ │ ├── index_suggester.go
│ │ ├── parse.go
│ │ └── parser
│ │ ├── collect_stmt_scopes.go
│ │ ├── collect_stmt_scopes_test.go
│ │ ├── collect_table_names.go
│ │ ├── collect_table_names_test.go
│ │ ├── collect_table_schemas.go
│ │ ├── collect_table_schemas_test.go
│ │ ├── errors.go
│ │ └── index.go
└── postgres
│ ├── pmodel
│ ├── explain_analyze_result_node.go
│ ├── explain_analyze_result_node_test.go
│ ├── explain_analyze_tree.go
│ └── explain_analyze_tree_node.go
│ └── pservice
│ ├── ast
│ └── ast_walker.go
│ ├── explain_analyze_tree_builder.go
│ ├── explain_analyze_tree_builder_test.go
│ ├── explainer.go
│ ├── explainer_test.go
│ ├── index_examiner.go
│ ├── index_getter.go
│ ├── index_suggester.go
│ ├── parse.go
│ └── parser
│ ├── collect_stmt_scopes.go
│ ├── collect_stmt_scopes_test.go
│ ├── collect_table_names.go
│ ├── collect_table_names_test.go
│ ├── collect_table_schemas.go
│ ├── collect_table_schemas_test.go
│ └── index.go
├── docker-compose.yml
├── docker
├── aws_mock
│ ├── Dockerfile
│ └── fastapi
│ │ ├── .gitignore
│ │ ├── data
│ │ ├── pi
│ │ │ ├── GetResourceMetrics_data.json
│ │ │ └── GetResourceMetrics_tokenized_data.json
│ │ └── rds
│ │ │ └── DescribeDbInstances.xml
│ │ ├── main.py
│ │ ├── pi.py
│ │ ├── rds.py
│ │ └── requirements.txt
├── go.mod
├── jaeger_mock
│ ├── Dockerfile
│ └── mock
│ │ ├── dumper.go
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── main.go
│ │ ├── mock_dependency_reader.go
│ │ ├── mock_span_reader.go
│ │ ├── mockdata
│ │ ├── 01_simple_trace.json
│ │ ├── 02_slow_trace.json
│ │ ├── 03_labs_trace.json
│ │ ├── 04_slow_trace_no_service.json
│ │ ├── 05_multiple_access.json
│ │ └── 06_same_db_system.json
│ │ └── trace.go
├── mysql8
│ └── init
│ │ └── script.sql
├── node
│ ├── Dockerfile
│ └── hasura
│ │ ├── config.yaml
│ │ └── metadata
│ │ ├── actions.graphql
│ │ ├── actions.yaml
│ │ ├── allow_list.yaml
│ │ ├── api_limits.yaml
│ │ ├── cron_triggers.yaml
│ │ ├── databases
│ │ ├── databases.yaml
│ │ └── default
│ │ │ └── tables
│ │ │ ├── public_tasks.yaml
│ │ │ ├── public_users.yaml
│ │ │ └── tables.yaml
│ │ ├── graphql_schema_introspection.yaml
│ │ ├── inherited_roles.yaml
│ │ ├── network.yaml
│ │ ├── query_collections.yaml
│ │ ├── remote_schemas.yaml
│ │ ├── rest_endpoints.yaml
│ │ └── version.yaml
└── postgres14
│ └── init
│ └── script.sql
├── docs
└── images
│ ├── jaeger.png
│ └── postgres.png
├── go.mod
├── go.sum
├── html
├── build_option.go
├── html_builder.go
└── viewmodel
│ ├── vm_db_load.go
│ ├── vm_db_load_of_sql.go
│ ├── vm_examination_command_option.go
│ ├── vm_examination_index_result.go
│ ├── vm_examination_result.go
│ ├── vm_index_column.go
│ ├── vm_index_target.go
│ ├── vm_mysql_explain_analyze_node.go
│ ├── vm_otel_compact_trace.go
│ ├── vm_otel_trace_span.go
│ ├── vm_postgres_explain_analyze_node.go
│ └── vm_time_db_load.go
├── infra
├── aws
│ ├── aws.go
│ ├── performance_insights.go
│ ├── performance_insights_test.go
│ ├── pi_sql_load_avg.go
│ ├── pi_sql_load_avg_test.go
│ ├── pi_time_value.go
│ ├── rds.go
│ ├── rds_db.go
│ └── rds_test.go
├── hasura
│ ├── client.go
│ ├── column_info.go
│ ├── config.go
│ ├── explain_request_body.go
│ ├── explain_response.go
│ ├── index_info.go
│ ├── query.go
│ ├── run_sql_args.go
│ └── run_sql_response.go
├── jaeger
│ ├── client.go
│ ├── client_test.go
│ └── trace.go
├── mysql
│ ├── column_info.go
│ ├── config.go
│ ├── db.go
│ ├── db_test.go
│ └── index_info.go
├── postgres
│ ├── column_info.go
│ ├── config.go
│ ├── db.go
│ ├── db_test.go
│ └── index_info.go
└── rdb
│ └── db.go
├── injection
├── bin_info.go
└── client_dist.go
├── lab
└── micro
│ ├── .gitignore
│ ├── bff
│ ├── Dockerfile
│ ├── package.json
│ ├── server.js
│ ├── tel.js
│ └── yarn.lock
│ ├── docker-compose.yml
│ ├── docker
│ └── user_db_mysql
│ │ └── init
│ │ └── script.sql
│ └── user
│ ├── Dockerfile
│ ├── db.go
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ └── otel.go
├── lib
├── env.go
├── env_test.go
├── errors.go
├── heap.go
├── heap_test.go
├── http_transport.go
├── join.go
├── join_test.go
├── ptr.go
├── set.go
├── set_test.go
├── slice.go
├── slice_test.go
├── sort.go
├── sort_test.go
├── stack.go
├── stack_test.go
├── time.go
├── time_test.go
├── uniq.go
└── uniq_test.go
├── main.go
├── mocks
└── jaeger
│ └── Reader.go
├── otel
├── jaeger
│ ├── trace_fetcher.go
│ ├── trace_fetcher_test.go
│ ├── trace_picker.go
│ ├── trace_picker_test.go
│ ├── v1_converter.go
│ └── v1_converter_test.go
└── omodel
│ ├── any_value.go
│ ├── compact_info_collect_visitor.go
│ ├── same_resource_count_visitor.go
│ ├── span.go
│ ├── span_test.go
│ ├── trace_compacter.go
│ ├── trace_tree.go
│ ├── trace_tree_test.go
│ └── trace_visitor.go
├── testdata
└── files
│ └── aws
│ ├── pi
│ ├── GetResourceMetrics_sql.json
│ └── GetResourceMetrics_tokenized_sql.json
│ └── rds
│ └── DescribeDbInstances.xml
└── thelper
├── bytes.go
├── db.go
├── file.go
├── injection.go
├── jaeger.go
├── setup.go
├── tdata
├── hasura_data.go
└── postgres_data.go
└── temp.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | example/* linguist-generated
2 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 | on: [push]
3 | jobs:
4 | go-test:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v3
8 | - uses: actions/setup-go@v3
9 | with:
10 | go-version-file: 'go.mod'
11 | cache: true
12 | - name: 'Create a dummy file to enable to build'
13 | run: mkdir -p client/dist/assets && touch client/dist/assets/.dummy
14 | - uses: golangci/golangci-lint-action@v3
15 | with:
16 | args: --timeout 5m --verbose
17 | - run: make test
18 | client-test:
19 | defaults:
20 | run:
21 | working-directory: client
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v3
25 | - uses: actions/setup-node@v3
26 | with:
27 | node-version: 16
28 | cache: 'yarn'
29 | cache-dependency-path: 'client/yarn.lock'
30 | - run: yarn install
31 | - run: yarn check:all
32 | - run: yarn test
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Go template
2 | # Binaries for programs and plugins
3 | *.exe
4 | *.exe~
5 | *.dll
6 | *.so
7 | *.dylib
8 |
9 | # Test binary, built with `go test -c`
10 | *.test
11 |
12 | # Output of the go coverage tool, specifically when used with LiteIDE
13 | *.out
14 |
15 | # Dependency directories (remove the comment below to include it)
16 | # vendor/
17 |
18 | docker/*/data
19 | dist/
20 | example/
21 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # Make sure to check the documentation at https://goreleaser.com
2 | project_name: GravityR
3 | dist: dist/goreleaser
4 | builds:
5 | - id: gr
6 | binary: gr
7 | env:
8 | - CGO_ENABLED=0
9 | goos:
10 | - linux
11 | - windows
12 | - darwin
13 | goarch:
14 | - amd64
15 | - arm64
16 | archives:
17 | - format: binary
18 | replacements:
19 | darwin: Darwin
20 | linux: Linux
21 | windows: Windows
22 | amd64: x86_64
23 | checksum:
24 | name_template: 'checksums.txt'
25 | snapshot:
26 | name_template: "{{ incpatch .Version }}-next"
27 | # changelog:
28 | # sort: asc
29 | # filters:
30 | # exclude:
31 | # - '^docs:'
32 | # - '^test:'
33 |
34 | # modelines, feel free to remove those if you don't want/use them:
35 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
36 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 mrasu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/client/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "overrides": [
3 | {
4 | "files": ["src/dummies/*.json"],
5 | "options": {
6 | "printWidth": 1000
7 | }
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/client/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["svelte.svelte-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | GravityR
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/jest.setup.ts:
--------------------------------------------------------------------------------
1 | import "reflect-metadata";
2 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "rm -rf ./dist && vite build -c vite-main.config.ts && vite build -c vite-mermaid.config.ts",
9 | "preview": "vite preview",
10 | "check:all": "yarn check:svelte && yarn check:prettier",
11 | "check:svelte": "svelte-check --fail-on-warnings --fail-on-hints --tsconfig ./tsconfig.json",
12 | "check:prettier": "prettier --check --ignore-path .gitignore \"!package-lock.json\" --plugin-search-dir=. ./**/*.{json,css,js,ts,cjs,svelte}",
13 | "test": "jest",
14 | "fix:prettier": "prettier --write --ignore-path .gitignore \"!package-lock.json\" --plugin-search-dir=. ./**/*.{json,css,js,ts,cjs,svelte}"
15 | },
16 | "devDependencies": {
17 | "@sveltejs/vite-plugin-svelte": "^1.0.1",
18 | "@testing-library/jest-dom": "^5.16.5",
19 | "@testing-library/svelte": "^3.2.1",
20 | "@tsconfig/svelte": "^3.0.0",
21 | "jest-environment-jsdom": "^29.0.3",
22 | "svelte": "^3.49.0",
23 | "svelte-check": "^2.8.0",
24 | "svelte-jester": "^2.3.2",
25 | "svelte-preprocess": "^4.10.7",
26 | "tslib": "^2.4.0",
27 | "typescript": "^4.6.4",
28 | "vite": "^3.0.0"
29 | },
30 | "dependencies": {
31 | "@types/jest": "^28.1.6",
32 | "apexcharts": "^3.35.3",
33 | "chota": "^0.8.0",
34 | "class-transformer": "^0.5.1",
35 | "jest": "^28.1.3",
36 | "mermaid": "^9.2.2",
37 | "prettier": "^2.7.1",
38 | "prettier-plugin-svelte": "^2.7.0",
39 | "reflect-metadata": "^0.1.13",
40 | "sql-formatter": "^10.6.0",
41 | "svelte-chota": "^1.8.6",
42 | "ts-jest": "^28.0.7",
43 | "ts-node": "^10.9.1"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/client/roots/mermaid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | GravityR
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/src/Mermaid.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 | {#if $grParam.dev}
25 |
32 | {/if}
33 |
34 | GravityR
35 |
36 | {#if page === "dig_jaeger"}
37 |
38 | {/if}
39 |
40 |
--------------------------------------------------------------------------------
/client/src/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | display: flex;
4 | min-width: 320px;
5 | min-height: 100vh;
6 | }
7 |
8 | h1 {
9 | font-size: 3.2em;
10 | line-height: 1.1;
11 | }
12 |
13 | #app {
14 | width: 1280px;
15 | margin: 0 auto;
16 | padding: 2rem;
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/components/DetailsCard.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 | {title}
12 |
13 |
14 |
15 |
16 |
21 |
--------------------------------------------------------------------------------
/client/src/components/ExaminationResultTable.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 | Times after adding index
11 |
12 |
13 |
14 |
15 | Execution Time |
16 | Reduction |
17 | Columns |
18 | SQL |
19 |
20 |
21 |
22 |
24 | {result.originalTimeMillis.toLocaleString()}ms
25 | |
27 | - |
28 | (Original) |
29 | |
30 |
31 |
32 | {#each sortedIndexResults as indexResult}
33 | {@const reducedPercent = indexResult.toReductionPercent(
34 | result.originalTimeMillis
35 | )}
36 |
37 |
38 | {indexResult.executionTimeMillis.toLocaleString()}ms |
40 | {reducedPercent}% |
41 | {indexResult.toIndex()} |
42 |
43 | {indexResult.indexTarget.toAlterAddSQL()}
44 |
45 | |
46 |
47 | {/each}
48 |
49 |
50 |
74 |
--------------------------------------------------------------------------------
/client/src/components/ExplainText.svelte:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 | {#each texts as text, i}
35 | {`${text}`}
36 | {/each}
37 | {#if trailingText.length > 0}
38 | -----------
39 | {trailingText}
40 | {/if}
41 |
42 |
43 |
55 |
--------------------------------------------------------------------------------
/client/src/components/IndexSuggestion.svelte:
--------------------------------------------------------------------------------
1 |
41 |
42 | Adding index to below columns might improve performance of the query.
43 |
44 |
45 | {#each indexTargets as it, index}
46 | -
47 |
48 | {it.toString()}
49 |
50 |
51 | {/each}
52 |
53 |
54 |
55 | With below command, you can examine the efficiency of indexes by temporarily
56 | adding them in your database.
57 |
58 |
59 |
60 | $ {commandText}
61 |
62 |
63 |
75 |
--------------------------------------------------------------------------------
/client/src/components/PlainText.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | {text.trim()}
6 |
7 |
15 |
--------------------------------------------------------------------------------
/client/src/contexts/HighlightIndexContext.ts:
--------------------------------------------------------------------------------
1 | import { getContext, setContext } from "svelte";
2 | import { writable } from "svelte/store";
3 | import type { Writable } from "svelte/types/runtime/store";
4 |
5 | type numberOrUndef = number | undefined;
6 |
7 | export function createHighlightIndexContext() {
8 | const symbol = Symbol();
9 | const x = writable(undefined);
10 | setContext>(symbol, x);
11 |
12 | return symbol;
13 | }
14 |
15 | export function getHighlightIndex(symbol: symbol): Writable {
16 | return getContext>(symbol);
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/dummies/DummyDigData.ts:
--------------------------------------------------------------------------------
1 | import type { IDbLoad, ITimeDbLoad } from "../types/gr_param";
2 | import digSqlDbLoads from "./digSqlDbLoads.json";
3 | import digTokenizedSqlDbLoads from "./digTokenizedSqlDbLoads.json";
4 | import jaegerTraces from "./jaegerTraces.json";
5 |
6 | type timeDbLoad = { timestamp: string; databases: IDbLoad[] };
7 |
8 | const toTimeDbLoads = (loads: timeDbLoad[]) => {
9 | const timeDbLoads: ITimeDbLoad[] = [];
10 | for (const sql of loads) {
11 | timeDbLoads.push({
12 | ...sql,
13 | timestamp: new Date(sql.timestamp).getTime(),
14 | });
15 | }
16 | return timeDbLoads;
17 | };
18 |
19 | export const dummyDigData = {
20 | performanceInsights: {
21 | sqlDbLoads: toTimeDbLoads(digSqlDbLoads),
22 | tokenizedSqlDbLoads: toTimeDbLoads(digTokenizedSqlDbLoads),
23 | },
24 | jaeger: jaegerTraces,
25 | };
26 |
--------------------------------------------------------------------------------
/client/src/dummies/hasuraPostgresQuery.sql:
--------------------------------------------------------------------------------
1 | SELECT coalesce(json_agg("root" ), '[]' ) AS "root" FROM (SELECT row_to_json((SELECT "_e" FROM (SELECT "_root.or.user"."user" AS "user", "_root.base"."description" AS "description", "_root.base"."id" AS "id", "_root.base"."status" AS "status" ) AS "_e" ) ) AS "root" FROM (SELECT * FROM "public"."tasks" WHERE (EXISTS (SELECT 1 FROM "public"."users" AS "__be_0_users" WHERE (((("__be_0_users"."id") = ("public"."tasks"."user_id")) AND ('true')) AND (('true') AND ((((("__be_0_users"."email") = (('test1111@example.com')::varchar)) AND ('true')) AND ('true')) AND ('true')))) )) ) AS "_root.base" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "_e" FROM (SELECT "_root.or.user.base"."email" AS "email", "_root.or.user.base"."id" AS "id", "_root.or.user.base"."name" AS "name" ) AS "_e" ) ) AS "user" FROM (SELECT * FROM "public"."users" WHERE (("_root.base"."user_id") = ("id")) LIMIT 1 ) AS "_root.or.user.base" ) AS "_root.or.user" ON ('true') ) AS "_root"
2 |
--------------------------------------------------------------------------------
/client/src/dummy.ts:
--------------------------------------------------------------------------------
1 | import { dummySuggestData } from "./dummies/DummySuggestData";
2 | import { dummyDigData } from "./dummies/DummyDigData";
3 |
4 | window.grParam = {
5 | dev: true,
6 | suggestData: dummySuggestData,
7 | digData: dummyDigData,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/lib/components/CommandCode.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 | {commandText}
8 |
9 |
10 |
16 |
--------------------------------------------------------------------------------
/client/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./app.css";
2 | import App from "./App.svelte";
3 |
4 | const app = new App({
5 | target: document.getElementById("app"),
6 | });
7 |
8 | export default app;
9 |
--------------------------------------------------------------------------------
/client/src/mermaid.ts:
--------------------------------------------------------------------------------
1 | import "./app.css";
2 | import Mermaid from "./Mermaid.svelte";
3 | import mermaid from "mermaid";
4 |
5 | mermaid.mermaidAPI.initialize({
6 | startOnLoad: false,
7 | sequence: { messageAlign: "right" },
8 | });
9 |
10 | const app = new Mermaid({
11 | target: document.getElementById("app"),
12 | });
13 |
14 | export default app;
15 |
--------------------------------------------------------------------------------
/client/src/models/BaseSuggestData.ts:
--------------------------------------------------------------------------------
1 | import type { IndexTarget } from "./IndexTarget";
2 | import { ExaminationCommandOption } from "./ExaminationCommandOption";
3 | import type { ExaminationResult } from "./ExaminationResult";
4 | import type {
5 | IDbSuggestData,
6 | IExaminationResult,
7 | IIndexTarget,
8 | } from "@/types/gr_param";
9 | import { plainToInstance } from "class-transformer";
10 |
11 | export abstract class BaseSuggestData {
12 | query: string;
13 | indexTargets?: IndexTarget[];
14 | examinationCommandOptions: ExaminationCommandOption[];
15 | examinationResult?: ExaminationResult;
16 |
17 | protected constructor(suggestData: IDbSuggestData) {
18 | this.query = suggestData.query;
19 | this.indexTargets = this.createIndexTargets(suggestData.indexTargets);
20 |
21 | this.examinationCommandOptions = suggestData.examinationCommandOptions?.map(
22 | (v) => plainToInstance(ExaminationCommandOption, v)
23 | );
24 |
25 | this.examinationResult = this.createExaminationResult(
26 | suggestData.examinationResult
27 | );
28 | }
29 |
30 | protected abstract createIndexTargets(
31 | targets?: IIndexTarget[]
32 | ): IndexTarget[];
33 |
34 | protected abstract createExaminationResult(
35 | result?: IExaminationResult
36 | ): ExaminationResult;
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/models/DbLoad.ts:
--------------------------------------------------------------------------------
1 | import { Type } from "class-transformer";
2 | import { DbLoadOfSql } from "./DbLoadOfSql";
3 |
4 | export class DbLoad {
5 | name: string;
6 |
7 | @Type(() => DbLoadOfSql)
8 | sqls: DbLoadOfSql[];
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/models/DbLoadOfSql.ts:
--------------------------------------------------------------------------------
1 | export class DbLoadOfSql {
2 | sql: string;
3 | loadMax: number;
4 | loadSum: number;
5 | tokenizedId: string;
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/models/DigData.ts:
--------------------------------------------------------------------------------
1 | import { plainToInstance, Type } from "class-transformer";
2 | import { PerformanceInsightsData } from "@/models/PerformanceInsightsData";
3 | import { JaegerData } from "@/models/JaegerData";
4 | import type { IDigData } from "@/types/gr_param";
5 |
6 | export class DigData {
7 | @Type(() => PerformanceInsightsData)
8 | performanceInsightsData?: PerformanceInsightsData;
9 |
10 | @Type(() => JaegerData)
11 | jaegerData?: JaegerData;
12 |
13 | constructor(iDigData: IDigData) {
14 | if (iDigData.performanceInsights) {
15 | this.performanceInsightsData = plainToInstance(
16 | PerformanceInsightsData,
17 | iDigData.performanceInsights
18 | );
19 | }
20 |
21 | if (iDigData.jaeger) {
22 | this.jaegerData = plainToInstance(JaegerData, iDigData.jaeger);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/models/ExaminationCommandOption.ts:
--------------------------------------------------------------------------------
1 | export class ExaminationCommandOption {
2 | name: string;
3 | value: string;
4 | isShort: boolean;
5 |
6 | ToCommandString(): string {
7 | const dash = this.isShort ? "-" : "--";
8 | const escapedValue = `${this.value.replace(/(["$`\\])/g, "\\$1")}`;
9 |
10 | if (escapedValue.match(/^\w+$/)) {
11 | return `${dash}${this.name} ${escapedValue}`;
12 | } else {
13 | return `${dash}${this.name} "${escapedValue}"`;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/models/ExaminationIndexResult.ts:
--------------------------------------------------------------------------------
1 | import { IndexTarget } from "./IndexTarget";
2 | import { Type } from "class-transformer";
3 |
4 | export class ExaminationIndexResult {
5 | @Type(() => IndexTarget)
6 | indexTarget: IndexTarget;
7 | executionTimeMillis: number;
8 |
9 | toReductionPercent(originalTimeMillis: number): string {
10 | return (
11 | (1 - this.executionTimeMillis / originalTimeMillis) *
12 | 100
13 | ).toLocaleString(undefined, {
14 | minimumFractionDigits: 2,
15 | maximumFractionDigits: 2,
16 | });
17 | }
18 |
19 | toIndex(): string {
20 | return this.indexTarget.toString();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/models/ExaminationResult.ts:
--------------------------------------------------------------------------------
1 | import { ExaminationIndexResult } from "./ExaminationIndexResult";
2 | import { Type } from "class-transformer";
3 |
4 | export class ExaminationResult {
5 | originalTimeMillis: number;
6 |
7 | @Type(() => ExaminationIndexResult)
8 | indexResults: ExaminationIndexResult[];
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/models/HasuraIndexTarget.ts:
--------------------------------------------------------------------------------
1 | import { PostgresIndexTarget } from "@/models/PostgresIndexTarget";
2 |
3 | export class HasuraIndexTarget extends PostgresIndexTarget {
4 | toExecutableText(): string {
5 | return `curl --request POST \
6 | --url "\${HASURA_URL}/v2/query" \
7 | --header 'Content-Type: application/json' \
8 | --header "x-hasura-admin-secret: \${HASURA_ADMIN_SECRET}" \
9 | --data '{
10 | "type": "run_sql",
11 | "args": {
12 | "sql": "${this.toAlterAddSQL()}"
13 | }
14 | }'`;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/models/HasuraSuggestData.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | IIndexTarget,
3 | IExaminationResult,
4 | IHasuraSuggestData,
5 | } from "@/types/gr_param";
6 | import { BaseSuggestData } from "./BaseSuggestData";
7 | import { plainToInstance } from "class-transformer";
8 | import type { IndexTarget } from "@/models/IndexTarget";
9 | import { ExaminationResult } from "@/models/ExaminationResult";
10 | import { format } from "sql-formatter";
11 | import { PostgresExplainData } from "@/models/explain_data/PostgresExplainData";
12 | import { HasuraIndexTarget } from "@/models/HasuraIndexTarget";
13 |
14 | export class HasuraSuggestData extends BaseSuggestData {
15 | gql: string;
16 | gqlVariables: Record;
17 | analyzeNodes?: PostgresExplainData[];
18 | summaryText: string;
19 |
20 | constructor(hasuraData: IHasuraSuggestData) {
21 | const suggestData = hasuraData.postgres;
22 | super(suggestData);
23 |
24 | this.query = format(suggestData.query, {
25 | language: "postgresql",
26 | });
27 |
28 | this.summaryText = suggestData.summaryText;
29 |
30 | this.gql = suggestData.gql;
31 | this.gqlVariables = suggestData.gqlVariables;
32 |
33 | this.analyzeNodes = plainToInstance(
34 | PostgresExplainData,
35 | suggestData.analyzeNodes
36 | );
37 | }
38 |
39 | protected createIndexTargets(targets?: IIndexTarget[] | null): IndexTarget[] {
40 | return targets?.map((v) => plainToInstance(HasuraIndexTarget, v));
41 | }
42 |
43 | protected createExaminationResult(
44 | result?: IExaminationResult
45 | ): ExaminationResult {
46 | const res = result ? plainToInstance(ExaminationResult, result) : undefined;
47 | if (!res) return res;
48 |
49 | for (const result of res.indexResults) {
50 | result.indexTarget = plainToInstance(
51 | HasuraIndexTarget,
52 | result.indexTarget
53 | );
54 | }
55 |
56 | return res;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/client/src/models/IndexColumn.ts:
--------------------------------------------------------------------------------
1 | export class IndexColumn {
2 | name: string;
3 | }
4 |
--------------------------------------------------------------------------------
/client/src/models/IndexTarget.ts:
--------------------------------------------------------------------------------
1 | import { IndexColumn } from "./IndexColumn";
2 | import { Type } from "class-transformer";
3 |
4 | export class IndexTarget {
5 | tableName: string;
6 | @Type(() => IndexColumn)
7 | columns: IndexColumn[];
8 |
9 | toString(): string {
10 | const columns = this.columns.map((v) => v.name).join(", ");
11 | return `Table=${this.tableName} / Column=${columns}`;
12 | }
13 |
14 | toGrIndexOption(): string {
15 | return `"${this.tableName}:${this.toGrColumnOption()}"`;
16 | }
17 |
18 | toAlterAddSQL(): string {
19 | const columns = this.columns.map((v) => v.name).join(", ");
20 |
21 | return `ALTER TABLE ${this.tableName} ADD INDEX (${columns});`;
22 | }
23 |
24 | toExecutableText(): string {
25 | return this.toAlterAddSQL();
26 | }
27 |
28 | private toGrColumnOption(): string {
29 | return this.columns.map((v) => v.name).join("+");
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/models/JaegerData.ts:
--------------------------------------------------------------------------------
1 | import { Type } from "class-transformer";
2 | import { OtelCompactedTrace } from "@/models/OtelCompactedTrace";
3 |
4 | export class JaegerData {
5 | uiPath: string;
6 | slowThresholdMilli: number;
7 | sameServiceThreshold: number;
8 |
9 | @Type(() => OtelCompactedTrace)
10 | slowTraces: OtelCompactedTrace[];
11 |
12 | @Type(() => OtelCompactedTrace)
13 | sameServiceTraces: OtelCompactedTrace[];
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/models/MysqlSuggestData.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | IMysqlSuggestData,
3 | IIndexTarget,
4 | IExaminationResult,
5 | } from "@/types/gr_param";
6 | import { BaseSuggestData } from "./BaseSuggestData";
7 | import { plainToInstance } from "class-transformer";
8 | import { IndexTarget } from "@/models/IndexTarget";
9 | import { ExaminationResult } from "@/models/ExaminationResult";
10 | import { MysqlAnalyzeData } from "@/models/explain_data/MysqlAnalyzeData";
11 |
12 | export class MysqlSuggestData extends BaseSuggestData {
13 | analyzeNodes?: MysqlAnalyzeData[];
14 |
15 | constructor(suggestData: IMysqlSuggestData) {
16 | super(suggestData);
17 |
18 | this.analyzeNodes = plainToInstance(
19 | MysqlAnalyzeData,
20 | suggestData.analyzeNodes
21 | );
22 | }
23 |
24 | protected createIndexTargets(targets?: IIndexTarget[]): IndexTarget[] {
25 | return targets?.map((v) => plainToInstance(IndexTarget, v));
26 | }
27 |
28 | protected createExaminationResult(
29 | result?: IExaminationResult
30 | ): ExaminationResult {
31 | return result ? plainToInstance(ExaminationResult, result) : undefined;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/models/OtelCompactedTrace.ts:
--------------------------------------------------------------------------------
1 | import { Type } from "class-transformer";
2 | import { OtelTraceSpan } from "@/models/OtelTraceSpan";
3 | import { TraceTree } from "@/models/trace_diagram/TraceTree";
4 |
5 | export class OtelCompactedTrace {
6 | traceId: string;
7 | sameServiceAccessCount: number;
8 | timeConsumingServiceName: string;
9 |
10 | @Type(() => OtelTraceSpan)
11 | root: OtelTraceSpan;
12 |
13 | toTraceTree(): TraceTree {
14 | const duration = this.root.endTimeMillis - this.root.startTimeMillis;
15 | const root = this.root.toTraceNode();
16 | return new TraceTree(
17 | this.traceId,
18 | duration,
19 | this.sameServiceAccessCount,
20 | this.timeConsumingServiceName,
21 | root
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/models/PerformanceInsightsData.ts:
--------------------------------------------------------------------------------
1 | import { Type } from "class-transformer";
2 | import { TimeDbLoad } from "@/models/TimeDbLoad";
3 |
4 | export class PerformanceInsightsData {
5 | @Type(() => TimeDbLoad)
6 | sqlDbLoads: TimeDbLoad[];
7 |
8 | @Type(() => TimeDbLoad)
9 | tokenizedSqlDbLoads: TimeDbLoad[];
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/models/PostgresIndexTarget.ts:
--------------------------------------------------------------------------------
1 | import { IndexTarget } from "@/models/IndexTarget";
2 |
3 | export class PostgresIndexTarget extends IndexTarget {
4 | toAlterAddSQL(): string {
5 | const columns = this.columns.map((v) => v.name).join(", ");
6 |
7 | return `CREATE INDEX ON ${this.tableName} (${columns});`;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/models/PostgresSuggestData.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | IIndexTarget,
3 | IPostgresSuggestData,
4 | IExaminationResult,
5 | } from "@/types/gr_param";
6 | import { BaseSuggestData } from "./BaseSuggestData";
7 | import { plainToInstance } from "class-transformer";
8 | import type { IndexTarget } from "@/models/IndexTarget";
9 | import { PostgresIndexTarget } from "@/models/PostgresIndexTarget";
10 | import { ExaminationResult } from "@/models/ExaminationResult";
11 | import { PostgresAnalyzeData } from "@/models/explain_data/PostgresAnalyzeData";
12 |
13 | export class PostgresSuggestData extends BaseSuggestData {
14 | analyzeNodes?: PostgresAnalyzeData[];
15 | summaryText: string;
16 |
17 | constructor(suggestData: IPostgresSuggestData) {
18 | super(suggestData);
19 |
20 | this.analyzeNodes = plainToInstance(
21 | PostgresAnalyzeData,
22 | suggestData.analyzeNodes
23 | );
24 | this.summaryText = suggestData.summaryText;
25 | }
26 |
27 | protected createIndexTargets(targets?: IIndexTarget[] | null): IndexTarget[] {
28 | return targets?.map((v) => plainToInstance(PostgresIndexTarget, v));
29 | }
30 |
31 | protected createExaminationResult(
32 | result?: IExaminationResult
33 | ): ExaminationResult {
34 | const res = result ? plainToInstance(ExaminationResult, result) : undefined;
35 | if (!res) return res;
36 |
37 | for (const result of res.indexResults) {
38 | result.indexTarget = plainToInstance(
39 | PostgresIndexTarget,
40 | result.indexTarget
41 | );
42 | }
43 |
44 | return res;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/client/src/models/SuggestData.ts:
--------------------------------------------------------------------------------
1 | import type { ISuggestData } from "@/types/gr_param";
2 | import { MysqlSuggestData } from "./MysqlSuggestData";
3 | import { PostgresSuggestData } from "./PostgresSuggestData";
4 | import { HasuraSuggestData } from "@/models/HasuraSuggestData";
5 |
6 | export class SuggestData {
7 | mysqlSuggestData?: MysqlSuggestData;
8 | postgresSuggestData?: PostgresSuggestData;
9 | hasuraSuggestData?: HasuraSuggestData;
10 |
11 | constructor(iSuggestData: ISuggestData) {
12 | if (iSuggestData.mysql) {
13 | this.mysqlSuggestData = new MysqlSuggestData(iSuggestData.mysql);
14 | }
15 |
16 | if (iSuggestData.postgres) {
17 | this.postgresSuggestData = new PostgresSuggestData(iSuggestData.postgres);
18 | }
19 |
20 | if (iSuggestData.hasura) {
21 | this.hasuraSuggestData = new HasuraSuggestData(iSuggestData.hasura);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/models/TimeDbLoad.ts:
--------------------------------------------------------------------------------
1 | import { Type } from "class-transformer";
2 | import { DbLoad } from "./DbLoad";
3 |
4 | export class TimeDbLoad {
5 | timestamp: number;
6 |
7 | @Type(() => DbLoad)
8 | databases: DbLoad[];
9 |
10 | get date(): Date {
11 | return new Date(this.timestamp);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/models/explain_data/DbAnalyzeData.ts:
--------------------------------------------------------------------------------
1 | import { DbExplainData } from "@/models/explain_data/DbExplainData";
2 |
3 | export abstract class DbAnalyzeData extends DbExplainData {
4 | actualTimeFirstRow?: number;
5 | actualTimeAvg?: number;
6 | actualReturnedRows?: number;
7 | actualLoopCount?: number;
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/models/explain_data/DbExplainData.ts:
--------------------------------------------------------------------------------
1 | export abstract class DbExplainData {
2 | text: string;
3 | title: string;
4 | tableName?: string;
5 |
6 | abstract children?: this[];
7 |
8 | abstract calculateEndTime(startTime: number): number;
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/models/explain_data/MysqlAnalyzeData.ts:
--------------------------------------------------------------------------------
1 | import { DbAnalyzeData } from "@/models/explain_data/DbAnalyzeData";
2 | import { Type } from "class-transformer";
3 | import "reflect-metadata";
4 |
5 | export class MysqlAnalyzeData extends DbAnalyzeData {
6 | @Type(() => MysqlAnalyzeData)
7 | children?: this[];
8 |
9 | estimatedInitCost?: number;
10 | estimatedCost?: number;
11 | estimatedReturnedRows?: number;
12 |
13 | calculateEndTime(startTime: number): number {
14 | if (this.actualLoopCount > 1) {
15 | if (this.actualLoopCount > 1000 && this.actualTimeAvg === 0) {
16 | // ActualTimeAvg doesn't show the value less than 0.000, however,
17 | // when the number of loop is much bigger, time can be meaningful even the result of multiplication is zero.
18 | // To handle the problem, assume ActualTimeAvg is some less than 0.000
19 | return startTime + 0.0001 * this.actualLoopCount;
20 | } else {
21 | return startTime + this.actualTimeAvg * this.actualLoopCount;
22 | }
23 | } else {
24 | return this.actualTimeAvg;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/models/explain_data/PostgresAnalyzeData.ts:
--------------------------------------------------------------------------------
1 | import { Type } from "class-transformer";
2 | import { DbAnalyzeData } from "@/models/explain_data/DbAnalyzeData";
3 |
4 | export class PostgresAnalyzeData extends DbAnalyzeData {
5 | @Type(() => PostgresAnalyzeData)
6 | children?: this[];
7 |
8 | estimatedInitCost: number;
9 | estimatedCost: number;
10 | estimatedReturnedRows: number;
11 | estimatedWidth: number;
12 |
13 | calculateEndTime(_startTime: number): number {
14 | // actualTimeAvg seems not the average time of loops for PostgreSQL even doc says so
15 | return this.actualTimeAvg;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/models/explain_data/PostgresExplainData.ts:
--------------------------------------------------------------------------------
1 | import { DbExplainData } from "@/models/explain_data/DbExplainData";
2 | import { Type } from "class-transformer";
3 |
4 | export class PostgresExplainData extends DbExplainData {
5 | @Type(() => PostgresExplainData)
6 | children?: this[];
7 |
8 | estimatedInitCost: number;
9 | estimatedCost: number;
10 | estimatedReturnedRows: number;
11 | estimatedWidth: number;
12 |
13 | calculateEndTime(_startTime: number): number {
14 | return this.estimatedCost;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/models/trace_diagram/ITraceNode.ts:
--------------------------------------------------------------------------------
1 | export abstract class ITraceNode {
2 | protected constructor(
3 | public serviceName: string,
4 | public children: ITraceNode[]
5 | ) {}
6 |
7 | abstract toMermaidStartArrow(fromServiceName: string): string;
8 | abstract toMermaidEndArrow(fromServiceName: string): string;
9 |
10 | hasChild(): boolean {
11 | return this.children.length > 0;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/models/trace_diagram/TraceNode.ts:
--------------------------------------------------------------------------------
1 | import { ITraceNode } from "@/models/trace_diagram/ITraceNode";
2 |
3 | export class TraceNode extends ITraceNode {
4 | constructor(serviceName: string, children: ITraceNode[] = []) {
5 | super(serviceName, children);
6 | }
7 |
8 | toMermaidStartArrow(fromServiceName: string): string {
9 | return `${fromServiceName}->>${this.serviceName}: ${this.serviceName}`;
10 | }
11 |
12 | toMermaidEndArrow(fromServiceName: string): string {
13 | return `${this.serviceName}->>${fromServiceName}: ${fromServiceName}`;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/models/trace_diagram/TraceRepeatNode.ts:
--------------------------------------------------------------------------------
1 | import { ITraceNode } from "@/models/trace_diagram/ITraceNode";
2 |
3 | const SERVICE_NAME = "repeat";
4 |
5 | export class TraceRepeatNode extends ITraceNode {
6 | constructor(
7 | public repeat: number,
8 | public toService: string,
9 | children: ITraceNode[] = []
10 | ) {
11 | super(SERVICE_NAME, children);
12 | }
13 |
14 | toMermaidStartArrow(fromServiceName: string): string {
15 | let texts = [];
16 | const repeatCount = Math.min(this.repeat, 3);
17 | for (let j = 0; j < repeatCount; j++) {
18 | if (j === 0) {
19 | const text = `${fromServiceName}->>${this.toService}: More ${this.repeat} times...`;
20 | texts.push(text);
21 | } else {
22 | texts.push(`${fromServiceName}->>${this.toService}: `);
23 | }
24 | }
25 |
26 | return texts.join("\n");
27 | }
28 |
29 | toMermaidEndArrow(_: string): string {
30 | return "";
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/models/trace_diagram/TraceTree.ts:
--------------------------------------------------------------------------------
1 | import type { ITraceNode } from "@/models/trace_diagram/ITraceNode";
2 |
3 | export class TraceTree {
4 | constructor(
5 | public traceId: string,
6 | public durationMilli: number,
7 | public sameServiceAccessCount: number,
8 | public timeConsumingServiceName: string,
9 | public root: ITraceNode
10 | ) {}
11 |
12 | toMermaidDiagram(): string {
13 | if (this.root.children.length === 0) {
14 | return this.toRootOnlyMermaidDiagram();
15 | }
16 |
17 | return "sequenceDiagram\r\n" + this.toMermaidDiagramRecursive(this.root);
18 | }
19 |
20 | private toRootOnlyMermaidDiagram(): string {
21 | return `
22 | sequenceDiagram
23 | ${this.root.serviceName}->>${this.root.serviceName}: ${" "}
24 | `;
25 | }
26 |
27 | private toMermaidDiagramRecursive(node: ITraceNode): string {
28 | let text = "";
29 | const serviceName = node.serviceName;
30 |
31 | for (let i = 0; i < node.children.length; i++) {
32 | const childTrace = node.children[i];
33 | text += childTrace.toMermaidStartArrow(serviceName) + "\n";
34 | if (childTrace.hasChild()) {
35 | text += `activate ${childTrace.serviceName}\n`;
36 | }
37 |
38 | text += this.toMermaidDiagramRecursive(childTrace);
39 |
40 | if (childTrace.hasChild()) {
41 | // Not show return arrow when the child trace is at the tail to make diagram smaller.
42 | text += childTrace.toMermaidEndArrow(serviceName) + "\n";
43 |
44 | text += `deactivate ${childTrace.serviceName}\n`;
45 | }
46 | }
47 | return text;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/pages/dig/jaeger/DigJaegerPage.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
26 |
27 |
28 |
29 |
38 |
39 |
--------------------------------------------------------------------------------
/client/src/pages/dig/jaeger/TraceDiagram.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
TraceId: {trace.traceId}
24 |
Open detail
25 |
26 |
27 |
28 |
30 |
--------------------------------------------------------------------------------
/client/src/pages/dig/performance_insights/DigPerformanceInsightsPage.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 | Plain
16 | Tokenized
17 |
18 |
19 |
20 | {#if activeTab === "plain"}
21 |
22 |
23 | Heaviest SQLs
24 |
25 |
26 |
27 |
28 |
29 |
30 | Heavy SQL Timeline
31 |
32 |
33 |
34 | {:else}
35 |
36 |
37 | Heaviest Tokenized SQLs
38 |
42 |
43 |
44 |
45 |
46 |
47 | Heavy Tokenized SQL Timeline
48 |
52 |
53 |
54 | {/if}
55 |
56 |
65 |
--------------------------------------------------------------------------------
/client/src/pages/dig/performance_insights/util/CommandText.ts:
--------------------------------------------------------------------------------
1 | export const buildCommandText = (dbName: string, sql: string): string => {
2 | const db = dbName ?? "...";
3 | const query = sql ?? "...";
4 |
5 | return `DB_DATABASE="${db}" gr db suggest -o "suggest.html" \\\n\t-q "${query}"`;
6 | };
7 |
--------------------------------------------------------------------------------
/client/src/pages/dig/performance_insights/util/Databases.ts:
--------------------------------------------------------------------------------
1 | import type { TimeDbLoad } from "@/models/TimeDbLoad";
2 |
3 | export const getAllDatabase = (dbLoads?: TimeDbLoad[]): string[] => {
4 | if (!dbLoads) return [];
5 |
6 | const dbs = dbLoads.map((c) => c.databases.map((d) => d.name)).flat();
7 | return Array.from(new Set(dbs)).sort();
8 | };
9 |
10 | export const getFirstDatabaseAsArray = (databases: string[]): string[] => {
11 | if (databases.length === 0) {
12 | return [];
13 | }
14 |
15 | return [databases[0]];
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/pages/dig/performance_insights/util/LoadTooltip.ts:
--------------------------------------------------------------------------------
1 | type LoadProp = {
2 | dbName: string;
3 | sql: string;
4 | loadMaxPercent: number;
5 | date: Date;
6 | };
7 |
8 | export const createLoadMaxTooltip = (
9 | { date, dbName, loadMaxPercent, sql }: LoadProp,
10 | maxHeight: number
11 | ): string => {
12 | return `
13 |
14 | ${date.toUTCString()}
15 | Database: ${dbName}
16 |
17 | db.load.avg:
18 |
19 | ${loadMaxPercent.toFixed(2)}%
20 |
21 |
22 |
${sql}
23 |
24 | `;
25 | };
26 |
27 | type LoadSumProp = {
28 | dbName: string;
29 | sql: string;
30 | loadSumPercent: number;
31 | };
32 |
33 | export const createLoadSumTooltip = (
34 | { dbName, loadSumPercent, sql }: LoadSumProp,
35 | maxHeight: number
36 | ): string => {
37 | return `
38 |
39 | Database: ${dbName}
40 |
41 | Sum of db.load.avg:
42 |
43 | ${loadSumPercent.toFixed(2)}%
44 |
45 |
46 |
${sql}
47 |
48 | `;
49 | };
50 |
--------------------------------------------------------------------------------
/client/src/pages/dig/performance_insights/util/chart/LoadPropChartSereies.ts:
--------------------------------------------------------------------------------
1 | import type { GrChartSeries, GrChartSeriesData } from "@/lib/GrChart";
2 | import type { TimeDbLoad } from "@/models/TimeDbLoad";
3 | import type { DbLoadOfSql } from "@/models/DbLoadOfSql";
4 | import type { DbLoad } from "@/models/DbLoad";
5 |
6 | type SeriesRecord = Record[number]>>;
7 |
8 | export const createLoadPropChartSeries = (
9 | dbLoads: TimeDbLoad[],
10 | targetDatabases: string[],
11 | threshold: number,
12 | buildDataFn: (
13 | timeDbLoad: TimeDbLoad,
14 | dbLoad: DbLoad,
15 | loadOfSql: DbLoadOfSql
16 | ) => GrChartSeriesData
17 | ): GrChartSeries => {
18 | let series: GrChartSeries = [];
19 | let knownSeries: SeriesRecord = {};
20 |
21 | for (let i = 0; i < dbLoads.length; i++) {
22 | const timeDbLoad = dbLoads[i];
23 | for (const dbLoad of timeDbLoad.databases) {
24 | if (!targetDatabases.includes(dbLoad.name)) continue;
25 |
26 | for (const loadOfSql of dbLoad.sqls) {
27 | if (loadOfSql.loadMax < threshold) continue;
28 |
29 | if (!knownSeries[dbLoad.name]) {
30 | knownSeries[dbLoad.name] = {};
31 | }
32 | const dbSeries = knownSeries[dbLoad.name];
33 | if (!dbSeries[loadOfSql.sql]) {
34 | const seriesData = {
35 | database: dbLoad.name,
36 | name: loadOfSql.sql,
37 | data: Array(i).fill({
38 | x: timeDbLoad.date,
39 | y: 0,
40 | }),
41 | };
42 | dbSeries[loadOfSql.sql] = seriesData;
43 | series.push(seriesData);
44 | }
45 |
46 | dbSeries[loadOfSql.sql].data.push(
47 | buildDataFn(timeDbLoad, dbLoad, loadOfSql)
48 | );
49 | }
50 | }
51 |
52 | for (const s of series) {
53 | if (s.data.length < i + 1) {
54 | s.data.push({
55 | x: timeDbLoad.date,
56 | y: 0,
57 | });
58 | }
59 | }
60 | }
61 |
62 | return series;
63 | };
64 |
--------------------------------------------------------------------------------
/client/src/pages/dig/performance_insights/util/chart/TokenizedLoad.ts:
--------------------------------------------------------------------------------
1 | import type { TimeDbLoad } from "@/models/TimeDbLoad";
2 | import type { DbLoadOfSql } from "@/models/DbLoadOfSql";
3 |
4 | export type RawLoadOfSql = {
5 | dbName: string;
6 | load: DbLoadOfSql;
7 | date: Date;
8 | };
9 |
10 | export const createRawLoads = (
11 | tokenizedLoads: TimeDbLoad[]
12 | ): Record => {
13 | const rawLoads = {};
14 |
15 | for (const load of tokenizedLoads) {
16 | for (const dbLoad of load.databases) {
17 | for (const loadOfSql of dbLoad.sqls) {
18 | if (!rawLoads[loadOfSql.tokenizedId]) {
19 | rawLoads[loadOfSql.tokenizedId] = [];
20 | }
21 | rawLoads[loadOfSql.tokenizedId].push({
22 | dbName: dbLoad.name,
23 | load: loadOfSql,
24 | date: load.date,
25 | });
26 | }
27 | }
28 | }
29 |
30 | return rawLoads;
31 | };
32 |
33 | export const pickRawLoads = (
34 | rawLoads: Record,
35 | size: number,
36 | tokenizedId: string
37 | ): RawLoadOfSql[] => {
38 | const loads = rawLoads[tokenizedId];
39 | const sortedLoads = [...loads].sort(
40 | (l1, l2) => l2.load.loadMax - l1.load.loadMax
41 | );
42 |
43 | return sortedLoads.slice(0, size);
44 | };
45 |
--------------------------------------------------------------------------------
/client/src/pages/suggest/hasura/ExaminationLineMenu.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
18 |
19 |
20 |
46 |
--------------------------------------------------------------------------------
/client/src/pages/suggest/mysql/SuggestMysqlPage.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {#if suggestData.query}
17 |
18 |
19 |
20 | {/if}
21 |
22 | {#if suggestData.analyzeNodes}
23 |
24 |
28 |
29 | {/if}
30 |
31 | {#if suggestData.analyzeNodes}
32 |
33 |
38 |
39 | {/if}
40 |
41 | {#if suggestData.indexTargets}
42 |
47 |
52 |
53 | {/if}
54 |
55 | {#if suggestData.examinationResult}
56 |
57 |
58 |
59 | {/if}
60 |
61 |
62 |
64 |
--------------------------------------------------------------------------------
/client/src/stores/gr_param.ts:
--------------------------------------------------------------------------------
1 | import { readable } from "svelte/store";
2 | import { SuggestData } from "@/models/SuggestData";
3 | import { DigData } from "@/models/DigData";
4 |
5 | export const grParam = readable({
6 | dev: window.grParam.dev || false,
7 | suggestData: window.grParam.suggestData
8 | ? new SuggestData(window.grParam.suggestData)
9 | : null,
10 |
11 | digData: window.grParam.digData ? new DigData(window.grParam.digData) : null,
12 | });
13 |
--------------------------------------------------------------------------------
/client/src/types/apexcharts.d.ts:
--------------------------------------------------------------------------------
1 | // Make possible to work autocomplete for the instance of ApexChart().
2 | import "apexcharts";
3 |
4 | type ChartW = { config: { series: T } };
5 | type ChartContext = { w: ChartW };
6 | type ChartPoint = {
7 | dataPointIndex: number;
8 | seriesIndex: number;
9 | w: ChartW;
10 | };
11 | type ChartConfig = { seriesIndex: number; dataPointIndex: number };
12 |
--------------------------------------------------------------------------------
/client/src/types/svelte-chota.d.ts:
--------------------------------------------------------------------------------
1 | declare module "svelte-chota" {
2 | import { SvelteComponentTyped } from "svelte";
3 | export class Tab extends SvelteComponentTyped {}
4 | export class Tabs extends SvelteComponentTyped {}
5 | export class Details extends SvelteComponentTyped {}
6 | export class Radio extends SvelteComponentTyped {}
7 | export class Checkbox extends SvelteComponentTyped {}
8 | export class Nav extends SvelteComponentTyped {}
9 | export class Card extends SvelteComponentTyped {}
10 | export class Button extends SvelteComponentTyped {}
11 | export class Modal extends SvelteComponentTyped {}
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/types/window.d.ts:
--------------------------------------------------------------------------------
1 | import type { IGrParam } from "./gr_param";
2 |
3 | declare global {
4 | interface Window {
5 | grParam: IGrParam;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/client/svelte.config.js:
--------------------------------------------------------------------------------
1 | import sveltePreprocess from "svelte-preprocess";
2 |
3 | export default {
4 | // Consult https://github.com/sveltejs/svelte-preprocess
5 | // for more information about preprocessors
6 | preprocess: sveltePreprocess(),
7 | };
8 |
--------------------------------------------------------------------------------
/client/test/src/pages/dig/jaeger/DigJaegerPage.test.ts:
--------------------------------------------------------------------------------
1 | import { plainToInstance } from "class-transformer";
2 | import { JaegerData } from "@/models/JaegerData";
3 | import { render, screen } from "@testing-library/svelte";
4 | import DigJaegerPage from "@/pages/dig/jaeger/DigJaegerPage.svelte";
5 |
6 | describe("DigJaegerPage", () => {
7 | describe("smoke test", () => {
8 | const jaegerData = plainToInstance(JaegerData, {
9 | uiPath: "http://localhost:16686",
10 | slowThresholdMilli: 100,
11 | sameServiceThreshold: 5,
12 | slowTraces: [],
13 | sameServiceTraces: [],
14 | });
15 |
16 | it("renders slowTraces", () => {
17 | render(DigJaegerPage, {
18 | jaegerData: jaegerData,
19 | });
20 | const data = screen.getByTestId("slowTraces");
21 | expect(data).toBeInTheDocument();
22 | });
23 |
24 | it("renders sameServiceTraces", () => {
25 | render(DigJaegerPage, {
26 | jaegerData: jaegerData,
27 | });
28 | const data = screen.getByTestId("sameServiceTraces");
29 | expect(data).toBeInTheDocument();
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/client/test/src/pages/dig/performance_insights/DigPerformanceInsightsPage.test.ts:
--------------------------------------------------------------------------------
1 | import DigPerformanceInsightsPage from "@/pages/dig/performance_insights/DigPerformanceInsightsPage.svelte";
2 | import { render, screen } from "@testing-library/svelte";
3 | import { plainToInstance } from "class-transformer";
4 | import { PerformanceInsightsData } from "@/models/PerformanceInsightsData";
5 | import ApexCharts from "apexcharts";
6 |
7 | describe("DigPerformanceInsightsPage", () => {
8 | beforeEach(() => {
9 | jest.spyOn(ApexCharts.prototype, "render").mockReturnValue(null);
10 | });
11 |
12 | describe("smoke test", () => {
13 | const performanceInsightsData = plainToInstance(PerformanceInsightsData, {
14 | sqlDbLoads: [],
15 | tokenizedSqlDbLoads: [],
16 | });
17 |
18 | it("renders sqls", () => {
19 | render(DigPerformanceInsightsPage, {
20 | performanceInsightsData: performanceInsightsData,
21 | });
22 | const data = screen.getByTestId("sqls");
23 | expect(data).toBeInTheDocument();
24 | });
25 |
26 | it("renders timeline", () => {
27 | render(DigPerformanceInsightsPage, {
28 | performanceInsightsData: performanceInsightsData,
29 | });
30 | const data = screen.getByTestId("timeline");
31 | expect(data).toBeInTheDocument();
32 | });
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/client/test/src/pages/suggest/mysql/SuggestMysqlPage.test.ts:
--------------------------------------------------------------------------------
1 | import SuggestMysqlPage from "@/pages/suggest/mysql/SuggestMysqlPage.svelte";
2 | import { render, screen } from "@testing-library/svelte";
3 | import { MysqlSuggestData } from "@/models/MysqlSuggestData";
4 | import ApexCharts from "apexcharts";
5 |
6 | describe("SuggestMysqlPage", () => {
7 | beforeEach(() => {
8 | jest.spyOn(ApexCharts.prototype, "render").mockReturnValue(null);
9 | jest.spyOn(ApexCharts.prototype, "destroy").mockReturnValue(null);
10 | });
11 |
12 | describe("smoke test", () => {
13 | const suggestData = new MysqlSuggestData({
14 | query: "SELECT * FROM users",
15 | indexTargets: [],
16 | examinationCommandOptions: [],
17 | examinationResult: {
18 | originalTimeMillis: 11,
19 | indexResults: [],
20 | },
21 | analyzeNodes: [],
22 | });
23 |
24 | it("renders sql", () => {
25 | render(SuggestMysqlPage, {
26 | suggestData: suggestData,
27 | });
28 | const data = screen.getByTestId("sql");
29 | expect(data).toBeInTheDocument();
30 | });
31 |
32 | it("renders explain", () => {
33 | render(SuggestMysqlPage, {
34 | suggestData: suggestData,
35 | });
36 | const data = screen.getByTestId("explain");
37 | expect(data).toBeInTheDocument();
38 | });
39 |
40 | it("renders explainChart", () => {
41 | render(SuggestMysqlPage, {
42 | suggestData: suggestData,
43 | });
44 | const data = screen.getByTestId("explainChart");
45 | expect(data).toBeInTheDocument();
46 | });
47 |
48 | it("renders suggest", () => {
49 | render(SuggestMysqlPage, {
50 | suggestData: suggestData,
51 | });
52 | const data = screen.getByTestId("suggest");
53 | expect(data).toBeInTheDocument();
54 | });
55 |
56 | it("renders examination", () => {
57 | render(SuggestMysqlPage, {
58 | suggestData: suggestData,
59 | });
60 | const data = screen.getByTestId("examination");
61 | expect(data).toBeInTheDocument();
62 | });
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/client/test/src/pages/suggest/postgres/SuggestPostgresPage.test.ts:
--------------------------------------------------------------------------------
1 | import SuggestPostgresPage from "@/pages/suggest/postgres/SuggestPostgresPage.svelte";
2 | import { render, screen } from "@testing-library/svelte";
3 | import { PostgresSuggestData } from "@/models/PostgresSuggestData";
4 | import ApexCharts from "apexcharts";
5 |
6 | describe("SuggestPostgresPage", () => {
7 | beforeEach(() => {
8 | jest.spyOn(ApexCharts.prototype, "render").mockReturnValue(null);
9 | jest.spyOn(ApexCharts.prototype, "destroy").mockReturnValue(null);
10 | });
11 |
12 | describe("smoke test", () => {
13 | const suggestData = new PostgresSuggestData({
14 | query: "SELECT * FROM users",
15 | indexTargets: [],
16 | examinationCommandOptions: [],
17 | examinationResult: {
18 | originalTimeMillis: 11,
19 | indexResults: [],
20 | },
21 | analyzeNodes: [],
22 | summaryText: "",
23 | });
24 |
25 | it("renders sql", () => {
26 | render(SuggestPostgresPage, {
27 | suggestData: suggestData,
28 | });
29 | const data = screen.getByTestId("sql");
30 | expect(data).toBeInTheDocument();
31 | });
32 |
33 | it("renders explain", () => {
34 | render(SuggestPostgresPage, {
35 | suggestData: suggestData,
36 | });
37 | const data = screen.getByTestId("explain");
38 | expect(data).toBeInTheDocument();
39 | });
40 |
41 | it("renders explainChart", () => {
42 | render(SuggestPostgresPage, {
43 | suggestData: suggestData,
44 | });
45 | const data = screen.getByTestId("explainChart");
46 | expect(data).toBeInTheDocument();
47 | });
48 |
49 | it("renders suggest", () => {
50 | render(SuggestPostgresPage, {
51 | suggestData: suggestData,
52 | });
53 | const data = screen.getByTestId("suggest");
54 | expect(data).toBeInTheDocument();
55 | });
56 |
57 | it("renders examination", () => {
58 | render(SuggestPostgresPage, {
59 | suggestData: suggestData,
60 | });
61 | const data = screen.getByTestId("examination");
62 | expect(data).toBeInTheDocument();
63 | });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 | "compilerOptions": {
4 | "experimentalDecorators": true,
5 | "target": "ESNext",
6 | "useDefineForClassFields": true,
7 | "module": "ESNext",
8 | "resolveJsonModule": true,
9 | "baseUrl": ".",
10 | "paths": {
11 | "@/*": ["./src/*"]
12 | },
13 | /**
14 | * Typecheck JS in `.svelte` and `.js` files by default.
15 | * Disable checkJs if you'd like to use dynamic types in JS.
16 | * Note that setting allowJs false does not prevent the use
17 | * of JS in `.svelte` files.
18 | */
19 | "allowJs": true,
20 | "checkJs": true,
21 | "isolatedModules": true
22 | },
23 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/client/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/client/vite-common.config.ts:
--------------------------------------------------------------------------------
1 | import { svelte } from "@sveltejs/vite-plugin-svelte";
2 |
3 | // https://vitejs.dev/config/
4 | export const config = {
5 | plugins: [svelte()],
6 | build: {
7 | emptyOutDir: false,
8 | rollupOptions: {
9 | input: ["src/dummy.ts"],
10 | output: {
11 | assetFileNames: "assets/[name].[ext]",
12 | entryFileNames: "assets/[name].js",
13 | },
14 | },
15 | },
16 | resolve: {
17 | alias: [{ find: "@", replacement: "/src" }],
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/client/vite-main.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import { config } from "./vite-common.config";
3 |
4 | config["build"]["rollupOptions"]["input"] = ["src/main.ts"];
5 |
6 | export default defineConfig(config);
7 |
--------------------------------------------------------------------------------
/client/vite-mermaid.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import { config } from "./vite-common.config";
3 |
4 | config["build"]["rollupOptions"]["input"] = ["src/mermaid.ts"];
5 |
6 | export default defineConfig(config);
7 |
--------------------------------------------------------------------------------
/client/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import { config } from "./vite-common.config";
3 |
4 | config["build"]["rollupOptions"]["input"] = [
5 | "src/main.ts",
6 | "src/mermaid.ts",
7 | "src/dummy.ts",
8 | ];
9 |
10 | export default defineConfig(config);
11 |
--------------------------------------------------------------------------------
/cmd/app.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/mrasu/GravityR/cmd/app"
5 | "github.com/mrasu/GravityR/cmd/flag"
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | // appCmd represents the db command
10 | var appCmd = &cobra.Command{
11 | Use: "app",
12 | Short: "Inspect entire application",
13 | }
14 |
15 | func init() {
16 | appCmd.AddCommand(app.DigCmd)
17 |
18 | flg := appCmd.PersistentFlags()
19 | flg.StringVarP(&flag.AppFlag.Output, "output", "o", "", "[Required] File name to output result html")
20 | err := cobra.MarkFlagRequired(flg, "output")
21 | if err != nil {
22 | panic(err)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/cmd/app/dig.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/mrasu/GravityR/cmd/app/dig"
5 | "github.com/spf13/cobra"
6 | )
7 |
8 | var DigCmd = &cobra.Command{
9 | Use: "dig",
10 | Short: "Dig application behavior",
11 | }
12 |
13 | func init() {
14 | DigCmd.AddCommand(dig.JaegerCmd)
15 | }
16 |
--------------------------------------------------------------------------------
/cmd/db.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/mrasu/GravityR/cmd/db"
5 | "github.com/mrasu/GravityR/cmd/flag"
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | // dbCmd represents the db command
10 | var dbCmd = &cobra.Command{
11 | Use: "db",
12 | Short: "Inspect DB-ish tool",
13 | }
14 |
15 | func init() {
16 | dbCmd.AddCommand(db.SuggestCmd)
17 | dbCmd.AddCommand(db.DigCmd)
18 |
19 | flg := dbCmd.PersistentFlags()
20 | flg.StringVarP(&flag.DbFlag.Output, "output", "o", "", "[Required] File name to output result html")
21 | err := cobra.MarkFlagRequired(flg, "output")
22 | if err != nil {
23 | panic(err)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/cmd/db/dig.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "github.com/mrasu/GravityR/cmd/db/dig"
5 | "github.com/spf13/cobra"
6 | )
7 |
8 | var DigCmd = &cobra.Command{
9 | Use: "dig",
10 | Short: "Dig database behavior",
11 | }
12 |
13 | func init() {
14 | DigCmd.AddCommand(dig.PerformanceInsightsCmd)
15 |
16 | // Here you will define your flags and configuration settings.
17 |
18 | // Cobra supports Persistent Flags which will work for this command
19 | // and all subcommands, e.g.:
20 | // SuggestCmd.PersistentFlags().String("foo", "", "A help for foo")
21 |
22 | // Cobra supports local flags which will only run when this command
23 | // flg := DigCmd.Flags()
24 | // flg.BoolVar(&runsExamination, "with-examine", false, "Examine query by adding index")
25 | }
26 |
--------------------------------------------------------------------------------
/cmd/db/suggest.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "github.com/mrasu/GravityR/cmd/db/suggest"
5 | "github.com/spf13/cobra"
6 | )
7 |
8 | var SuggestCmd = &cobra.Command{
9 | Use: "suggest",
10 | Short: "Inspect and suggest",
11 | }
12 |
13 | func init() {
14 | SuggestCmd.AddCommand(suggest.MySqlCmd)
15 | SuggestCmd.AddCommand(suggest.PostgresCmd)
16 | SuggestCmd.AddCommand(suggest.HasuraCmd)
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/db/suggest/hasura.go:
--------------------------------------------------------------------------------
1 | package suggest
2 |
3 | import (
4 | "github.com/mrasu/GravityR/cmd/db/suggest/hasura"
5 | "github.com/spf13/cobra"
6 | )
7 |
8 | var HasuraCmd = &cobra.Command{
9 | Use: "hasura",
10 | Short: "Suggest ways to increase Hasura's performance",
11 | }
12 |
13 | func init() {
14 | HasuraCmd.AddCommand(hasura.PostgresCmd)
15 | }
16 |
--------------------------------------------------------------------------------
/cmd/flag/app_flag.go:
--------------------------------------------------------------------------------
1 | package flag
2 |
3 | var AppFlag = &appFlag{}
4 |
5 | type appFlag struct {
6 | Output string
7 | }
8 |
--------------------------------------------------------------------------------
/cmd/flag/db_flag.go:
--------------------------------------------------------------------------------
1 | package flag
2 |
3 | var DbFlag = &dbFlag{}
4 |
5 | type dbFlag struct {
6 | Output string
7 | }
8 |
--------------------------------------------------------------------------------
/cmd/flag/global_flag.go:
--------------------------------------------------------------------------------
1 | package flag
2 |
3 | var GlobalFlag = &globalFlag{}
4 |
5 | type globalFlag struct {
6 | Verbose bool
7 | UseMock bool
8 | }
9 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/mrasu/GravityR/cmd/flag"
5 | "github.com/mrasu/GravityR/cmd/util"
6 | "github.com/rs/zerolog"
7 | "github.com/rs/zerolog/log"
8 | "os"
9 |
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | // rootCmd represents the base command when called without any subcommands
14 | var rootCmd = &cobra.Command{
15 | Use: "gr",
16 | SilenceUsage: true,
17 | SilenceErrors: true,
18 | Short: "Gravity Radar to remove bottleneck in your application",
19 | Long: `GravityR is Gravity-Radar.
20 | This exists to reduce time to find bottleneck in your application.
21 | And also this is to solve the problems faster and easier.
22 | `,
23 | PersistentPreRun: func(cmd *cobra.Command, args []string) {
24 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
25 | if flag.GlobalFlag.Verbose {
26 | zerolog.SetGlobalLevel(zerolog.DebugLevel)
27 | } else {
28 | zerolog.SetGlobalLevel(zerolog.InfoLevel)
29 | }
30 |
31 | if flag.GlobalFlag.UseMock {
32 | log.Info().Msg("NOTE: Using mock")
33 | }
34 | },
35 | }
36 |
37 | // Execute adds all child commands to the root command and sets flags appropriately.
38 | // This is called by main.main(). It only needs to happen once to the rootCmd.
39 | func Execute() {
40 | err := rootCmd.Execute()
41 | if err != nil {
42 | util.LogError(err)
43 | os.Exit(1)
44 | }
45 | }
46 |
47 | func init() {
48 | rootCmd.AddCommand(dbCmd)
49 | rootCmd.AddCommand(appCmd)
50 | rootCmd.AddCommand(versionCmd)
51 |
52 | rootCmd.PersistentFlags().BoolVar(&flag.GlobalFlag.UseMock, "use-mock", false, "use mock (for development)")
53 | rootCmd.PersistentFlags().BoolVarP(&flag.GlobalFlag.Verbose, "verbose", "v", false, "verbose output")
54 | }
55 |
--------------------------------------------------------------------------------
/cmd/util/log.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/rs/zerolog"
6 | "github.com/rs/zerolog/log"
7 | "os"
8 | "path"
9 | )
10 |
11 | func LogError(err error) {
12 | if zerolog.GlobalLevel() <= zerolog.DebugLevel {
13 | log.Error().Msgf("%+v", err)
14 | } else {
15 | log.Error().Msg(err.Error())
16 | }
17 | }
18 |
19 | func LogResultOutputPath(outputPath string) {
20 | wd, err := os.Getwd()
21 | if err == nil {
22 | log.Info().Msg("Result html is at: " + path.Join(wd, outputPath))
23 | }
24 | }
25 |
26 | func LogNewIndexTargets(newIdxTargets []*dmodel.IndexTarget) {
27 | if len(newIdxTargets) > 0 {
28 | log.Debug().Msg("Found possibly efficient index combinations:")
29 | for i, it := range newIdxTargets {
30 | log.Printf("\t%d.%s", i, it.CombinationString())
31 | }
32 | } else {
33 | log.Debug().Msg("No possibly efficient index found. Perhaps already indexed?")
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/injection"
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | var versionCmd = &cobra.Command{
10 | Use: "version",
11 | Short: "Show version",
12 | Run: func(cmd *cobra.Command, args []string) {
13 | fmt.Printf("GravityR v%s, commit %s\n", injection.BinInfo.Version, injection.BinInfo.Commit)
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/database/dmodel/examination_index_result.go:
--------------------------------------------------------------------------------
1 | package dmodel
2 |
3 | import (
4 | "github.com/mrasu/GravityR/html/viewmodel"
5 | )
6 |
7 | type ExaminationIndexResult struct {
8 | IndexTarget *IndexTarget
9 | ExecutionTimeMillis int64
10 | }
11 |
12 | func NewExaminationIndexResult(it *IndexTarget, time int64) *ExaminationIndexResult {
13 | return &ExaminationIndexResult{
14 | IndexTarget: it,
15 | ExecutionTimeMillis: time,
16 | }
17 | }
18 |
19 | func (eir *ExaminationIndexResult) ToViewModel() *viewmodel.VmExaminationIndexResult {
20 | return &viewmodel.VmExaminationIndexResult{
21 | ExecutionTimeMillis: eir.ExecutionTimeMillis,
22 | IndexTarget: eir.IndexTarget.ToViewModel(),
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/database/dmodel/examination_result.go:
--------------------------------------------------------------------------------
1 | package dmodel
2 |
3 | import "github.com/mrasu/GravityR/html/viewmodel"
4 |
5 | type ExaminationResult struct {
6 | OriginalTimeMillis int64
7 | IndexResults []*ExaminationIndexResult
8 | }
9 |
10 | func NewExaminationResult(irs []*ExaminationIndexResult, time int64) *ExaminationResult {
11 | return &ExaminationResult{
12 | OriginalTimeMillis: time,
13 | IndexResults: irs,
14 | }
15 | }
16 |
17 | func (er *ExaminationResult) ToViewModel() *viewmodel.VmExaminationResult {
18 | var virs []*viewmodel.VmExaminationIndexResult
19 | for _, ir := range er.IndexResults {
20 | virs = append(virs, ir.ToViewModel())
21 | }
22 |
23 | return &viewmodel.VmExaminationResult{
24 | OriginalTimeMillis: er.OriginalTimeMillis,
25 | IndexResults: virs,
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/dmodel/index_column.go:
--------------------------------------------------------------------------------
1 | package dmodel
2 |
3 | import (
4 | "github.com/mrasu/GravityR/html/viewmodel"
5 | "github.com/pkg/errors"
6 | "regexp"
7 | )
8 |
9 | type IndexColumn struct {
10 | Name string
11 | }
12 |
13 | func NewIndexColumn(name string) (*IndexColumn, error) {
14 | if !wordOnlyReg.MatchString(name) {
15 | return nil, errors.Errorf("Including non word character. %s", name)
16 | }
17 |
18 | return &IndexColumn{Name: name}, nil
19 | }
20 |
21 | func (ic *IndexColumn) ToViewModel() *viewmodel.VmIndexColumn {
22 | return &viewmodel.VmIndexColumn{Name: ic.Name}
23 | }
24 |
25 | var nonAsciiReg = regexp.MustCompile(`\W`)
26 |
27 | func (ic *IndexColumn) SafeName() string {
28 | return nonAsciiReg.ReplaceAllString(ic.Name, "_")
29 | }
30 |
31 | func (ic *IndexColumn) Equals(other *IndexColumn) bool {
32 | return ic.Name == other.Name
33 | }
34 |
--------------------------------------------------------------------------------
/database/dmodel/single_table_explain_result.go:
--------------------------------------------------------------------------------
1 | package dmodel
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type SingleTableExplainResult struct {
8 | TableName string
9 |
10 | EstimatedTotalTime float64
11 | }
12 |
13 | func (st *SingleTableExplainResult) String() string {
14 | txt := fmt.Sprintf(
15 | "SingleTableExplainResult(TableName: %s, EstimatedTotalTime: %f)",
16 | st.TableName,
17 | st.EstimatedTotalTime,
18 | )
19 | return txt
20 | }
21 |
--------------------------------------------------------------------------------
/database/dservice/build_single_table_explain_results.go:
--------------------------------------------------------------------------------
1 | package dservice
2 |
3 | import "github.com/mrasu/GravityR/database/dmodel"
4 |
5 | type ExplainNode interface {
6 | TableName() string
7 | EstimatedTotalTime() float64
8 |
9 | GetChildren() []ExplainNode
10 | }
11 |
12 | type singleTableTree struct {
13 | tableName string
14 | nodes []ExplainNode
15 | }
16 |
17 | type singleTableExplainResultsBuilder struct{}
18 |
19 | func BuildSingleTableExplainResults(root ExplainNode) []*dmodel.SingleTableExplainResult {
20 | var trees []*singleTableTree
21 | sb := singleTableExplainResultsBuilder{}
22 | sb.toSingleTableTreeRecursive(&trees, root, nil)
23 |
24 | var res []*dmodel.SingleTableExplainResult
25 | for _, tree := range trees {
26 | res = append(res, &dmodel.SingleTableExplainResult{
27 | TableName: tree.tableName,
28 | EstimatedTotalTime: tree.nodes[0].EstimatedTotalTime(),
29 | })
30 | }
31 | return res
32 | }
33 |
34 | func (sb *singleTableExplainResultsBuilder) toSingleTableTreeRecursive(trees *[]*singleTableTree, node ExplainNode, currentTree *singleTableTree) bool {
35 | treeUsed := false
36 | isRoot := false
37 | if currentTree == nil {
38 | if node.TableName() != "" {
39 | currentTree = &singleTableTree{
40 | tableName: node.TableName(),
41 | nodes: []ExplainNode{node},
42 | }
43 | isRoot = true
44 | }
45 | } else {
46 | if node.TableName() == currentTree.tableName {
47 | currentTree.nodes = append(currentTree.nodes, node)
48 | treeUsed = true
49 | } else {
50 | *trees = append(*trees, currentTree)
51 |
52 | if node.TableName() != "" {
53 | currentTree = &singleTableTree{
54 | tableName: node.TableName(),
55 | nodes: []ExplainNode{node},
56 | }
57 | isRoot = true
58 | } else {
59 | currentTree = nil
60 | }
61 | }
62 | }
63 |
64 | if len(node.GetChildren()) == 0 && isRoot {
65 | *trees = append(*trees, currentTree)
66 | return false
67 | }
68 |
69 | for _, c := range node.GetChildren() {
70 | used := sb.toSingleTableTreeRecursive(trees, c, currentTree)
71 | if used {
72 | if isRoot {
73 | *trees = append(*trees, currentTree)
74 | }
75 | currentTree = nil
76 | treeUsed = true
77 | }
78 | }
79 |
80 | return treeUsed
81 | }
82 |
--------------------------------------------------------------------------------
/database/dservice/dparser/column_schema.go:
--------------------------------------------------------------------------------
1 | package dparser
2 |
3 | type ColumnSchema struct {
4 | Name string
5 | }
6 |
--------------------------------------------------------------------------------
/database/dservice/dparser/field.go:
--------------------------------------------------------------------------------
1 | package dparser
2 |
3 | type Field struct {
4 | AsName string
5 | Columns []*FieldColumn
6 | }
7 |
8 | func (f *Field) Name() string {
9 | if f.AsName != "" {
10 | return f.AsName
11 | } else if len(f.Columns) == 1 {
12 | return f.Columns[0].Name
13 | }
14 |
15 | return ""
16 | }
17 |
--------------------------------------------------------------------------------
/database/dservice/dparser/field_column.go:
--------------------------------------------------------------------------------
1 | package dparser
2 |
3 | type FieldColumn struct {
4 | // internal name to link with parent scope
5 | ReferenceName string
6 |
7 | Table string
8 | Name string
9 | Type FieldType
10 | }
11 |
--------------------------------------------------------------------------------
/database/dservice/dparser/field_type.go:
--------------------------------------------------------------------------------
1 | package dparser
2 |
3 | type FieldType int
4 |
5 | const (
6 | FieldReference FieldType = iota
7 | FieldCondition
8 | FieldAggregation
9 | FieldSubquery
10 | FieldStar
11 | )
12 |
--------------------------------------------------------------------------------
/database/dservice/dparser/index_field.go:
--------------------------------------------------------------------------------
1 | package dparser
2 |
3 | type IndexField struct {
4 | Name string
5 | Type FieldType
6 | }
7 |
--------------------------------------------------------------------------------
/database/dservice/dparser/index_target_table.go:
--------------------------------------------------------------------------------
1 | package dparser
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/database/dmodel"
6 | "github.com/mrasu/GravityR/lib"
7 | "sort"
8 | )
9 |
10 | type IndexTargetTable struct {
11 | TableName string
12 | IndexFields []*IndexField
13 | }
14 |
15 | func (itt *IndexTargetTable) ToIndexTarget() *dmodel.IndexTarget {
16 | it := &dmodel.IndexTarget{
17 | TableName: itt.TableName,
18 | }
19 | for _, f := range itt.IndexFields {
20 | it.Columns = append(it.Columns, &dmodel.IndexColumn{Name: f.Name})
21 | }
22 | return it
23 | }
24 |
25 | func (itt *IndexTargetTable) String() string {
26 | txt := fmt.Sprintf(
27 | "IndexTargetTable(table: %s columns: %s)",
28 | itt.TableName,
29 | lib.Join(itt.IndexFields, ", ", func(f *IndexField) string { return f.Name }),
30 | )
31 | return txt
32 | }
33 |
34 | func SortIndexTargetTable(tables []*IndexTargetTable) {
35 | sort.Slice(tables, func(i, j int) bool {
36 | if tables[i].TableName != tables[j].TableName {
37 | return tables[i].TableName < tables[j].TableName
38 | }
39 |
40 | iIndexes := tables[i].IndexFields
41 | jIndexes := tables[j].IndexFields
42 | if len(iIndexes) != len(jIndexes) {
43 | return len(iIndexes) < len(jIndexes)
44 | }
45 |
46 | for idx := 0; idx < len(iIndexes); idx++ {
47 | if iIndexes[idx].Name == jIndexes[idx].Name {
48 | continue
49 | }
50 | return iIndexes[idx].Name < jIndexes[idx].Name
51 | }
52 |
53 | return false
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/database/dservice/dparser/stmt_scope.go:
--------------------------------------------------------------------------------
1 | package dparser
2 |
3 | import "github.com/mrasu/GravityR/lib"
4 |
5 | const RootScopeName = ""
6 |
7 | type StmtScope struct {
8 | Name string
9 | Fields []*Field
10 | FieldScopes []*StmtScope
11 | Tables []*Table
12 | SubScopes []*StmtScope
13 | CTEs map[string]*StmtScope
14 | }
15 |
16 | func (ss *StmtScope) ListAsTableMap() map[string]*lib.Set[string] {
17 | res := map[string]*lib.Set[string]{}
18 | for _, t := range ss.Tables {
19 | res[t.AsName] = lib.NewSetS([]string{t.Name})
20 | }
21 |
22 | for _, s := range ss.SubScopes {
23 | asTableMap := s.ListAsTableMap()
24 | ts := lib.NewSet[string]()
25 | for name, tNames := range asTableMap {
26 | //TODO: consider scope to handle name duplication
27 | if s, ok := res[name]; ok {
28 | s.Merge(tNames)
29 | } else {
30 | res[name] = lib.NewSetS(tNames.Values())
31 | }
32 |
33 | ts.Merge(tNames)
34 | }
35 | res[s.Name] = ts
36 | }
37 |
38 | delete(res, "")
39 | return res
40 | }
41 |
--------------------------------------------------------------------------------
/database/dservice/dparser/table.go:
--------------------------------------------------------------------------------
1 | package dparser
2 |
3 | type Table struct {
4 | AsName string
5 | Name string
6 | IsLateral bool
7 | }
8 |
9 | func (t *Table) AsOrName() string {
10 | if t.AsName != "" {
11 | return t.AsName
12 | } else {
13 | return t.Name
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/database/dservice/dparser/table_schema.go:
--------------------------------------------------------------------------------
1 | package dparser
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/lib"
6 | )
7 |
8 | type TableSchema struct {
9 | Name string
10 | Columns []*ColumnSchema
11 | PrimaryKeys []string
12 | }
13 |
14 | func CreateTableSchemas[T any](tables []string, vals []T, f func(T) (string, string, bool)) []*TableSchema {
15 | ts := map[string]*TableSchema{}
16 | for _, v := range vals {
17 | var t *TableSchema
18 | tName, colName, isPK := f(v)
19 | if et, ok := ts[tName]; ok {
20 | t = et
21 | } else {
22 | t = &TableSchema{
23 | Name: tName,
24 | }
25 | ts[tName] = t
26 | }
27 |
28 | t.Columns = append(t.Columns, &ColumnSchema{
29 | Name: colName,
30 | })
31 |
32 | if isPK {
33 | t.PrimaryKeys = append(t.PrimaryKeys, colName)
34 | }
35 | }
36 |
37 | var res []*TableSchema
38 | for _, table := range tables {
39 | if t, ok := ts[table]; ok {
40 | res = append(res, t)
41 | } else {
42 | res = append(res, nil)
43 | }
44 | }
45 |
46 | return res
47 | }
48 |
49 | func (ts *TableSchema) TableDescription() string {
50 | txt := fmt.Sprintf(
51 | "Table(name: %s, columns: [%s], primaryKeys: %s)",
52 | ts.Name,
53 | lib.Join(ts.Columns, ", ", func(c *ColumnSchema) string { return c.Name }),
54 | ts.PrimaryKeys,
55 | )
56 | return txt
57 | }
58 |
--------------------------------------------------------------------------------
/database/dservice/existing_index_remover.go:
--------------------------------------------------------------------------------
1 | package dservice
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/samber/lo"
6 | )
7 |
8 | type IndexGetter interface {
9 | GetIndexes(database string, tables []string) ([]*dmodel.IndexTarget, error)
10 | }
11 |
12 | type ExistingIndexRemover struct {
13 | idxGetter IndexGetter
14 | idxTargets []*dmodel.IndexTarget
15 | dbName string
16 | }
17 |
18 | func NewExistingIndexRemover(idxGetter IndexGetter, dbName string, its []*dmodel.IndexTarget) *ExistingIndexRemover {
19 | return &ExistingIndexRemover{
20 | idxGetter: idxGetter,
21 | idxTargets: its,
22 | dbName: dbName,
23 | }
24 | }
25 |
26 | func (r *ExistingIndexRemover) Remove() ([]*dmodel.IndexTarget, error) {
27 | idxTargets := lo.Uniq(r.idxTargets)
28 |
29 | tNames := lo.Map(idxTargets, func(it *dmodel.IndexTarget, _ int) string { return it.TableName })
30 | tNames = lo.Uniq(tNames)
31 |
32 | existingIdxes, err := r.idxGetter.GetIndexes(r.dbName, tNames)
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | its := r.removeMatchedIndexTargets(existingIdxes, idxTargets)
38 |
39 | return its, nil
40 | }
41 |
42 | func (r *ExistingIndexRemover) removeMatchedIndexTargets(idxInfos []*dmodel.IndexTarget, its []*dmodel.IndexTarget) []*dmodel.IndexTarget {
43 | idxGroups := lo.GroupBy(idxInfos, func(i *dmodel.IndexTarget) string { return i.TableName })
44 |
45 | return lo.Filter(its, func(it *dmodel.IndexTarget, _ int) bool {
46 | if idxes, ok := idxGroups[it.TableName]; ok {
47 | for _, id := range idxes {
48 | if it.HasSameIdxColumns(id) {
49 | return false
50 | }
51 | }
52 | return true
53 | } else {
54 | return true
55 | }
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/database/hasura/hservice/index_examiner.go:
--------------------------------------------------------------------------------
1 | package hservice
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/database/dmodel"
6 | "github.com/mrasu/GravityR/infra/hasura"
7 | "github.com/mrasu/GravityR/lib"
8 | "time"
9 | )
10 |
11 | type IndexExaminer struct {
12 | cli *hasura.Client
13 | query string
14 | variables map[string]interface{}
15 | }
16 |
17 | func NewIndexExaminer(cli *hasura.Client, query string, v map[string]interface{}) *IndexExaminer {
18 | return &IndexExaminer{
19 | cli: cli,
20 | query: query,
21 | variables: v,
22 | }
23 | }
24 |
25 | func (ie *IndexExaminer) Execute() (int64, error) {
26 | start := time.Now()
27 | err := ie.cli.QueryWithoutResult(ie.query, ie.variables)
28 | if err != nil {
29 | return 0, err
30 | }
31 | elapsed := time.Since(start)
32 |
33 | return elapsed.Milliseconds(), nil
34 | }
35 |
36 | func (ie *IndexExaminer) CreateIndex(name string, it *dmodel.IndexTarget) error {
37 | sql := fmt.Sprintf(`CREATE INDEX "%s" ON "%s" (%s)`,
38 | name, it.TableName,
39 | lib.Join(it.Columns, ",", func(i *dmodel.IndexColumn) string { return `"` + i.SafeName() + `"` }),
40 | )
41 | _, err := ie.cli.RunRawSQL(sql)
42 | return err
43 | }
44 |
45 | func (ie *IndexExaminer) DropIndex(name string, it *dmodel.IndexTarget) error {
46 | sql := fmt.Sprintf(`DROP INDEX "%s"`, name)
47 | _, err := ie.cli.RunRawSQL(sql)
48 | return err
49 | }
50 |
--------------------------------------------------------------------------------
/database/hasura/hservice/index_getter.go:
--------------------------------------------------------------------------------
1 | package hservice
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/mrasu/GravityR/infra/hasura"
6 | "github.com/samber/lo"
7 | )
8 |
9 | type IndexGetter struct {
10 | cli *hasura.Client
11 | }
12 |
13 | func NewIndexGetter(cli *hasura.Client) *IndexGetter {
14 | return &IndexGetter{cli: cli}
15 | }
16 |
17 | func (ig *IndexGetter) GetIndexes(dbSchema string, tables []string) ([]*dmodel.IndexTarget, error) {
18 | infos, err := ig.cli.GetIndexes(dbSchema, tables)
19 | if err != nil {
20 | return nil, err
21 | }
22 |
23 | return lo.Map(infos, func(info *hasura.IndexInfo, _ int) *dmodel.IndexTarget {
24 | return dmodel.NewIndexTarget(info.TableName, info.Columns)
25 | }), nil
26 | }
27 |
--------------------------------------------------------------------------------
/database/hasura/hservice/index_suggester.go:
--------------------------------------------------------------------------------
1 | package hservice
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/mrasu/GravityR/database/dservice"
6 | "github.com/mrasu/GravityR/database/hasura/hservice/parser"
7 | "github.com/mrasu/GravityR/database/postgres/pservice"
8 | "github.com/mrasu/GravityR/infra/hasura"
9 | )
10 |
11 | type IndexSuggester struct {
12 | cli *hasura.Client
13 | }
14 |
15 | func NewIndexSuggester(cli *hasura.Client) *IndexSuggester {
16 | return &IndexSuggester{
17 | cli: cli,
18 | }
19 | }
20 |
21 | func (is *IndexSuggester) Suggest(query string) ([]*dmodel.IndexTarget, error) {
22 | stmt, err := pservice.Parse(query)
23 | if err != nil {
24 | return nil, err
25 | }
26 |
27 | its, errs := parser.ListPossibleIndexes(is.cli, stmt)
28 | if len(errs) > 0 {
29 | return nil, errs[0]
30 | }
31 |
32 | its, err = is.removeExistingIndexTargets(its)
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | return its, nil
38 | }
39 |
40 | func (is *IndexSuggester) removeExistingIndexTargets(its []*dmodel.IndexTarget) ([]*dmodel.IndexTarget, error) {
41 | idxGetter := NewIndexGetter(is.cli)
42 | res, err := dservice.NewExistingIndexRemover(idxGetter, "public", its).Remove()
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | return res, nil
48 | }
49 |
--------------------------------------------------------------------------------
/database/hasura/hservice/parser/collect_table_schemas.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/database/dservice/dparser"
6 | "github.com/mrasu/GravityR/infra/hasura"
7 | "github.com/mrasu/GravityR/lib"
8 | "github.com/rs/zerolog/log"
9 | )
10 |
11 | func CollectTableSchemas(cli *hasura.Client, schema string, tables []string) ([]*dparser.TableSchema, error) {
12 | cols, err := cli.GetTableColumns(schema, tables)
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | schemas := dparser.CreateTableSchemas(tables, cols, func(c *hasura.ColumnInfo) (string, string, bool) {
18 | return c.TableName, c.ColumnName, c.IsPK
19 | })
20 |
21 | for i, table := range tables {
22 | if schemas[i] == nil {
23 | return nil, lib.NewUnsupportedError(fmt.Sprintf("unknown table found. perhaps using VIEW? not supporting: %s", table))
24 | }
25 | }
26 |
27 | log.Debug().Msg("Table schemas:")
28 | for i, s := range schemas {
29 | log.Printf("\t%d. %s", i, s.TableDescription())
30 | }
31 |
32 | return schemas, nil
33 | }
34 |
--------------------------------------------------------------------------------
/database/hasura/hservice/parser/index.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | pParser "github.com/auxten/postgresql-parser/pkg/sql/parser"
5 | "github.com/mrasu/GravityR/database/dmodel"
6 | "github.com/mrasu/GravityR/database/dservice/dparser"
7 | "github.com/mrasu/GravityR/database/postgres/pservice/parser"
8 | "github.com/mrasu/GravityR/infra/hasura"
9 | )
10 |
11 | func ListPossibleIndexes(cli *hasura.Client, stmt *pParser.Statement) ([]*dmodel.IndexTarget, []error) {
12 | tNames, errs := parser.CollectTableNames(stmt)
13 | if len(errs) > 0 {
14 | return nil, errs
15 | }
16 | tables, err := CollectTableSchemas(cli, "public", tNames)
17 | if err != nil {
18 | return nil, []error{err}
19 | }
20 |
21 | scopes, errs := parser.CollectStmtScopes(stmt, "public")
22 | if len(errs) > 0 {
23 | return nil, errs
24 | }
25 |
26 | its, err := dparser.NewIndexTargetBuilder(tables).Build(scopes)
27 | if err != nil {
28 | return nil, []error{err}
29 | }
30 |
31 | return its, nil
32 | }
33 |
--------------------------------------------------------------------------------
/database/mysql/mmodel/analyze_result_line.go:
--------------------------------------------------------------------------------
1 | package mmodel
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // AnalyzeResultLine is the parsed information from `EXPLAIN ANALYZE` query
8 | // c.f) https://dev.mysql.com/doc/refman/8.0/en/explain.html#explain-analyze
9 | type AnalyzeResultLine struct {
10 | Text string
11 | TableName string
12 |
13 | EstimatedInitCost float64
14 | EstimatedCost float64
15 | EstimatedReturnedRows int
16 |
17 | ActualTimeFirstRow float64
18 | ActualTimeAvg float64
19 | ActualReturnedRows int
20 | ActualLoopCount int
21 | }
22 |
23 | func (l *AnalyzeResultLine) ActualTotalTime() float64 {
24 | return l.ActualTimeAvg * float64(l.ActualLoopCount)
25 | }
26 |
27 | func (l *AnalyzeResultLine) String() string {
28 | return fmt.Sprintf(
29 | "[%s] estimate_cost=%f, estimate_rows=%d, time=%f..%f, totalTime=%f, rows=%d, loops=%d\n%s",
30 | l.TableName,
31 | l.EstimatedCost, l.EstimatedReturnedRows,
32 | l.ActualTimeFirstRow, l.ActualTimeAvg, l.ActualTotalTime(),
33 | l.ActualReturnedRows, l.ActualLoopCount,
34 | l.Text,
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/database/mysql/mmodel/explain_analyze_tree.go:
--------------------------------------------------------------------------------
1 | package mmodel
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/mrasu/GravityR/database/dservice"
6 | "github.com/mrasu/GravityR/html/viewmodel"
7 | )
8 |
9 | type ExplainAnalyzeTree struct {
10 | Root *ExplainAnalyzeTreeNode
11 | }
12 |
13 | func (eat *ExplainAnalyzeTree) ToSingleTableResults() []*dmodel.SingleTableExplainResult {
14 | return dservice.BuildSingleTableExplainResults(eat.Root)
15 | }
16 |
17 | func (eat *ExplainAnalyzeTree) ToViewModel() []*viewmodel.VmMysqlExplainAnalyzeNode {
18 | var res []*viewmodel.VmMysqlExplainAnalyzeNode
19 | for _, n := range eat.Root.Children {
20 | res = append(res, n.ToViewModel())
21 | }
22 | return res
23 | }
24 |
--------------------------------------------------------------------------------
/database/mysql/mmodel/explain_analyze_tree_node.go:
--------------------------------------------------------------------------------
1 | package mmodel
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dservice"
5 | "github.com/mrasu/GravityR/html/viewmodel"
6 | )
7 |
8 | type ExplainAnalyzeTreeNode struct {
9 | AnalyzeResultLine *ExplainAnalyzeResultLine
10 | Children []*ExplainAnalyzeTreeNode
11 | }
12 |
13 | func (n *ExplainAnalyzeTreeNode) ToViewModel() *viewmodel.VmMysqlExplainAnalyzeNode {
14 | vm := &viewmodel.VmMysqlExplainAnalyzeNode{
15 | Text: n.AnalyzeResultLine.Text,
16 | Title: n.AnalyzeResultLine.Title(),
17 | TableName: n.AnalyzeResultLine.TableName,
18 | EstimatedInitCost: n.AnalyzeResultLine.EstimatedInitCost,
19 | EstimatedCost: n.AnalyzeResultLine.EstimatedCost,
20 | EstimatedReturnedRows: n.AnalyzeResultLine.EstimatedReturnedRows,
21 | ActualTimeFirstRow: n.AnalyzeResultLine.ActualTimeFirstRow,
22 | ActualTimeAvg: n.AnalyzeResultLine.ActualTimeAvg,
23 | ActualReturnedRows: n.AnalyzeResultLine.ActualReturnedRows,
24 | ActualLoopCount: n.AnalyzeResultLine.ActualLoopCount,
25 | }
26 | for _, c := range n.Children {
27 | vm.Children = append(vm.Children, c.ToViewModel())
28 | }
29 |
30 | return vm
31 | }
32 |
33 | func (n *ExplainAnalyzeTreeNode) TableName() string {
34 | return n.AnalyzeResultLine.TableName
35 | }
36 |
37 | func (n *ExplainAnalyzeTreeNode) EstimatedTotalTime() float64 {
38 | return n.AnalyzeResultLine.EstimatedTotalTime()
39 | }
40 |
41 | func (n *ExplainAnalyzeTreeNode) GetChildren() []dservice.ExplainNode {
42 | var res []dservice.ExplainNode
43 | for _, c := range n.Children {
44 | res = append(res, c)
45 | }
46 | return res
47 | }
48 |
--------------------------------------------------------------------------------
/database/mysql/mservice/explainer.go:
--------------------------------------------------------------------------------
1 | package mservice
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/mysql/mmodel"
5 | "github.com/mrasu/GravityR/infra/mysql"
6 | "github.com/mrasu/GravityR/lib"
7 | "github.com/pkg/errors"
8 | "strings"
9 | )
10 |
11 | type Explainer struct {
12 | db *mysql.DB
13 | }
14 |
15 | func NewExplainer(db *mysql.DB) *Explainer {
16 | return &Explainer{db: db}
17 | }
18 |
19 | func (e *Explainer) ExplainWithAnalyze(query string) (*mmodel.ExplainAnalyzeTree, error) {
20 | explainLine, err := e.db.ExplainWithAnalyze(query)
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | root, err := e.buildExplainNode(explainLine)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | return &mmodel.ExplainAnalyzeTree{Root: root}, nil
31 | }
32 |
33 | func (e *Explainer) buildExplainNode(explainLine string) (*mmodel.ExplainAnalyzeTreeNode, error) {
34 | nodeStack := lib.NewStack[mmodel.ExplainAnalyzeTreeNode]()
35 |
36 | root := &mmodel.ExplainAnalyzeTreeNode{AnalyzeResultLine: &mmodel.ExplainAnalyzeResultLine{}}
37 | nodeStack.Push(root)
38 | treeLines := strings.Split(explainLine, "\n")
39 | for _, line := range treeLines {
40 | if line == "" {
41 | continue
42 | }
43 |
44 | nest, l, err := mmodel.ParseExplainAnalyzeResultLine(line)
45 | if err != nil {
46 | return nil, err
47 | }
48 | n := &mmodel.ExplainAnalyzeTreeNode{
49 | AnalyzeResultLine: l,
50 | Children: nil,
51 | }
52 | if nodeStack.Size()-1 == nest {
53 | currentNode := nodeStack.Top()
54 | currentNode.Children = append(currentNode.Children, n)
55 | } else if nodeStack.Size()-1 > nest {
56 | nodeStack.Pop()
57 | for nodeStack.Size()-1 > nest {
58 | nodeStack.Pop()
59 | }
60 | currentNode := nodeStack.Top()
61 | currentNode.Children = append(currentNode.Children, n)
62 | } else {
63 | return nil, errors.New("invalid result from EXPLAIN ANALYZE")
64 | }
65 |
66 | nodeStack.Push(n)
67 | }
68 |
69 | return root, nil
70 | }
71 |
--------------------------------------------------------------------------------
/database/mysql/mservice/index_examiner.go:
--------------------------------------------------------------------------------
1 | package mservice
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/database/dmodel"
6 | "github.com/mrasu/GravityR/infra/mysql"
7 | "github.com/mrasu/GravityR/lib"
8 | "time"
9 | )
10 |
11 | type IndexExaminer struct {
12 | db *mysql.DB
13 | query string
14 | }
15 |
16 | func NewIndexExaminer(db *mysql.DB, query string) *IndexExaminer {
17 | return &IndexExaminer{
18 | db: db,
19 | query: query,
20 | }
21 | }
22 |
23 | func (ie *IndexExaminer) Execute() (int64, error) {
24 | start := time.Now()
25 | _, err := ie.db.Exec(ie.query)
26 | if err != nil {
27 | return 0, err
28 | }
29 | elapsed := time.Since(start)
30 |
31 | return elapsed.Milliseconds(), nil
32 | }
33 |
34 | func (ie *IndexExaminer) CreateIndex(name string, it *dmodel.IndexTarget) error {
35 | sql := fmt.Sprintf(
36 | "ALTER TABLE `%s` ADD INDEX `%s` (%s)",
37 | it.TableName, name,
38 | lib.Join(it.Columns, ",", func(i *dmodel.IndexColumn) string { return "`" + i.SafeName() + "`" }),
39 | )
40 | _, err := ie.db.Exec(sql)
41 | return err
42 | }
43 |
44 | func (ie *IndexExaminer) DropIndex(name string, it *dmodel.IndexTarget) error {
45 | sql := fmt.Sprintf("ALTER TABLE `%s` DROP INDEX `%s`", it.TableName, name)
46 | _, err := ie.db.Exec(sql)
47 | return err
48 | }
49 |
--------------------------------------------------------------------------------
/database/mysql/mservice/index_getter.go:
--------------------------------------------------------------------------------
1 | package mservice
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/mrasu/GravityR/infra/mysql"
6 | "github.com/samber/lo"
7 | )
8 |
9 | type IndexGetter struct {
10 | db *mysql.DB
11 | }
12 |
13 | func NewIndexGetter(db *mysql.DB) *IndexGetter {
14 | return &IndexGetter{db: db}
15 | }
16 |
17 | func (ig *IndexGetter) GetIndexes(dbName string, tables []string) ([]*dmodel.IndexTarget, error) {
18 | infos, err := ig.db.GetIndexes(dbName, tables)
19 | if err != nil {
20 | return nil, err
21 | }
22 |
23 | return lo.Map(infos, func(info *mysql.IndexInfo, _ int) *dmodel.IndexTarget {
24 | return dmodel.NewIndexTarget(info.TableName, info.Columns)
25 | }), nil
26 | }
27 |
--------------------------------------------------------------------------------
/database/mysql/mservice/index_suggester.go:
--------------------------------------------------------------------------------
1 | package mservice
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/mrasu/GravityR/database/dservice"
6 | "github.com/mrasu/GravityR/database/mysql/mservice/parser"
7 | "github.com/mrasu/GravityR/infra/mysql"
8 | tParser "github.com/pingcap/tidb/parser"
9 | )
10 |
11 | type IndexSuggester struct {
12 | db *mysql.DB
13 | dbName string
14 | }
15 |
16 | func NewIndexSuggester(db *mysql.DB, dbName string) *IndexSuggester {
17 | return &IndexSuggester{
18 | db: db,
19 | dbName: dbName,
20 | }
21 | }
22 |
23 | func (is *IndexSuggester) Suggest(query string) ([]*dmodel.IndexTarget, error) {
24 | p := tParser.New()
25 | rootNode, err := Parse(p, query)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | its, errs := parser.ListPossibleIndexes(is.db, is.dbName, rootNode)
31 | if len(errs) > 0 {
32 | return nil, errs[0]
33 | }
34 |
35 | its, err = is.removeExistingIndexTargets(its)
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | return its, nil
41 | }
42 |
43 | func (is *IndexSuggester) removeExistingIndexTargets(its []*dmodel.IndexTarget) ([]*dmodel.IndexTarget, error) {
44 | idxGetter := NewIndexGetter(is.db)
45 | res, err := dservice.NewExistingIndexRemover(idxGetter, is.dbName, its).Remove()
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | return res, nil
51 | }
52 |
--------------------------------------------------------------------------------
/database/mysql/mservice/parse.go:
--------------------------------------------------------------------------------
1 | package mservice
2 |
3 | import (
4 | "github.com/pingcap/tidb/parser"
5 | "github.com/pingcap/tidb/parser/ast"
6 | )
7 |
8 | func Parse(p *parser.Parser, query string) (ast.StmtNode, error) {
9 | stmtNodes, _, err := p.Parse(query, "", "")
10 | if err != nil {
11 | return nil, err
12 | }
13 |
14 | return stmtNodes[0], nil
15 | }
16 |
--------------------------------------------------------------------------------
/database/mysql/mservice/parser/collect_table_schemas.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/database/dservice/dparser"
6 | "github.com/mrasu/GravityR/infra/mysql"
7 | "github.com/mrasu/GravityR/lib"
8 | "github.com/rs/zerolog/log"
9 | )
10 |
11 | func CollectTableSchemas(db *mysql.DB, dbName string, tables []string) ([]*dparser.TableSchema, error) {
12 | cols, err := db.GetTableColumns(dbName, tables)
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | schemas := dparser.CreateTableSchemas(tables, cols, func(c *mysql.ColumnInfo) (string, string, bool) {
18 | return c.TableName, c.ColumnName, c.ColumnKey == "PRI"
19 | })
20 |
21 | for i, table := range tables {
22 | if schemas[i] == nil {
23 | return nil, lib.NewUnsupportedError(fmt.Sprintf("unknown table found. perhaps using VIEW? not supporting: %s", table))
24 | }
25 | }
26 |
27 | log.Debug().Msg("Table schemas:")
28 | for i, s := range schemas {
29 | log.Printf("\t%d. %s", i, s.TableDescription())
30 | }
31 |
32 | return schemas, nil
33 | }
34 |
--------------------------------------------------------------------------------
/database/mysql/mservice/parser/errors.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "fmt"
5 | "github.com/pingcap/tidb/parser/ast"
6 | "github.com/pkg/errors"
7 | "reflect"
8 | )
9 |
10 | type UnexpectedChildError struct {
11 | parent ast.Node
12 | child ast.Node
13 | }
14 |
15 | func NewUnexpectedChildError(p, c ast.Node) error {
16 | return errors.Wrap(&UnexpectedChildError{p, c}, "")
17 | }
18 |
19 | func (uc *UnexpectedChildError) Error() string {
20 | return fmt.Sprintf("parent: %s, child: %s", reflect.TypeOf(uc.parent).Name(), reflect.TypeOf(uc.child).Name())
21 | }
22 |
--------------------------------------------------------------------------------
/database/mysql/mservice/parser/index.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/mrasu/GravityR/database/dservice/dparser"
6 | "github.com/mrasu/GravityR/infra/mysql"
7 | "github.com/pingcap/tidb/parser/ast"
8 | )
9 |
10 | func ListPossibleIndexes(db *mysql.DB, dbName string, rootNode ast.StmtNode) ([]*dmodel.IndexTarget, []error) {
11 | tNames, errs := CollectTableNames(rootNode)
12 | if len(errs) > 0 {
13 | return nil, errs
14 | }
15 |
16 | tables, err := CollectTableSchemas(db, dbName, tNames)
17 | if err != nil {
18 | return nil, []error{err}
19 | }
20 |
21 | scopes, errs := CollectStmtScopes(rootNode)
22 | if len(errs) > 0 {
23 | return nil, errs
24 | }
25 |
26 | its, err := dparser.NewIndexTargetBuilder(tables).Build(scopes)
27 | if err != nil {
28 | return nil, []error{err}
29 | }
30 |
31 | return its, nil
32 | }
33 |
--------------------------------------------------------------------------------
/database/postgres/pmodel/explain_analyze_tree.go:
--------------------------------------------------------------------------------
1 | package pmodel
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/mrasu/GravityR/database/dservice"
6 | "github.com/mrasu/GravityR/html/viewmodel"
7 | )
8 |
9 | type ExplainAnalyzeTree struct {
10 | Root *ExplainAnalyzeTreeNode
11 | SummaryText string
12 | }
13 |
14 | func (eat *ExplainAnalyzeTree) ToSingleTableResults() []*dmodel.SingleTableExplainResult {
15 | return dservice.BuildSingleTableExplainResults(eat.Root)
16 | }
17 |
18 | func (eat *ExplainAnalyzeTree) ToViewModel() []*viewmodel.VmPostgresExplainAnalyzeNode {
19 | var res []*viewmodel.VmPostgresExplainAnalyzeNode
20 | for _, n := range eat.Root.Children {
21 | res = append(res, n.ToViewModel())
22 | }
23 | return res
24 | }
25 |
--------------------------------------------------------------------------------
/database/postgres/pmodel/explain_analyze_tree_node.go:
--------------------------------------------------------------------------------
1 | package pmodel
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dservice"
5 | "github.com/mrasu/GravityR/html/viewmodel"
6 | "strings"
7 | )
8 |
9 | type ExplainAnalyzeTreeNode struct {
10 | AnalyzeResultNode *ExplainAnalyzeResultNode
11 | Children []*ExplainAnalyzeTreeNode
12 | SpaceSize int
13 | }
14 |
15 | func (n *ExplainAnalyzeTreeNode) ToViewModel() *viewmodel.VmPostgresExplainAnalyzeNode {
16 | text := strings.Join(n.AnalyzeResultNode.Lines, "\n")
17 |
18 | vm := &viewmodel.VmPostgresExplainAnalyzeNode{
19 | Text: text,
20 | Title: n.AnalyzeResultNode.Title(),
21 | TableName: n.AnalyzeResultNode.TableName,
22 | EstimatedInitCost: n.AnalyzeResultNode.EstimatedInitCost,
23 | EstimatedCost: n.AnalyzeResultNode.EstimatedCost,
24 | EstimatedReturnedRows: n.AnalyzeResultNode.EstimatedReturnedRows,
25 | EstimatedWidth: n.AnalyzeResultNode.EstimatedWidth,
26 | ActualTimeFirstRow: n.AnalyzeResultNode.ActualTimeFirstRow,
27 | ActualTimeAvg: n.AnalyzeResultNode.ActualTimeAvg,
28 | ActualReturnedRows: n.AnalyzeResultNode.ActualReturnedRows,
29 | ActualLoopCount: n.AnalyzeResultNode.ActualLoopCount,
30 | }
31 | for _, c := range n.Children {
32 | vm.Children = append(vm.Children, c.ToViewModel())
33 | }
34 |
35 | return vm
36 | }
37 |
38 | func (n *ExplainAnalyzeTreeNode) TableName() string {
39 | return n.AnalyzeResultNode.TableName
40 | }
41 |
42 | func (n *ExplainAnalyzeTreeNode) EstimatedTotalTime() float64 {
43 | return n.AnalyzeResultNode.EstimatedTotalTime()
44 | }
45 |
46 | func (n *ExplainAnalyzeTreeNode) GetChildren() []dservice.ExplainNode {
47 | var res []dservice.ExplainNode
48 | for _, c := range n.Children {
49 | res = append(res, c)
50 | }
51 | return res
52 | }
53 |
--------------------------------------------------------------------------------
/database/postgres/pservice/explainer.go:
--------------------------------------------------------------------------------
1 | package pservice
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/postgres/pmodel"
5 | "github.com/mrasu/GravityR/infra/postgres"
6 | )
7 |
8 | type Explainer struct {
9 | db *postgres.DB
10 | }
11 |
12 | func NewExplainer(db *postgres.DB) *Explainer {
13 | return &Explainer{
14 | db: db,
15 | }
16 | }
17 |
18 | func (e *Explainer) ExplainWithAnalyze(query string) (*pmodel.ExplainAnalyzeTree, error) {
19 | explainLines, err := e.db.ExplainWithAnalyze(query)
20 | if err != nil {
21 | return nil, err
22 | }
23 |
24 | return NewExplainAnalyzeTreeBuilder().Build(explainLines)
25 | }
26 |
--------------------------------------------------------------------------------
/database/postgres/pservice/index_examiner.go:
--------------------------------------------------------------------------------
1 | package pservice
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/database/dmodel"
6 | "github.com/mrasu/GravityR/infra/postgres"
7 | "github.com/mrasu/GravityR/lib"
8 | "time"
9 | )
10 |
11 | type IndexExaminer struct {
12 | db *postgres.DB
13 | query string
14 | }
15 |
16 | func NewIndexExaminer(db *postgres.DB, query string) *IndexExaminer {
17 | return &IndexExaminer{
18 | db: db,
19 | query: query,
20 | }
21 | }
22 |
23 | func (ie *IndexExaminer) Execute() (int64, error) {
24 | start := time.Now()
25 | _, err := ie.db.Exec(ie.query)
26 | if err != nil {
27 | return 0, err
28 | }
29 | elapsed := time.Since(start)
30 |
31 | return elapsed.Milliseconds(), nil
32 | }
33 |
34 | func (ie *IndexExaminer) CreateIndex(name string, it *dmodel.IndexTarget) error {
35 | sql := fmt.Sprintf(`CREATE INDEX "%s" ON "%s" (%s)`,
36 | name, it.TableName,
37 | lib.Join(it.Columns, ",", func(i *dmodel.IndexColumn) string { return `"` + i.SafeName() + `"` }),
38 | )
39 | _, err := ie.db.Exec(sql)
40 | return err
41 | }
42 |
43 | func (ie *IndexExaminer) DropIndex(name string, it *dmodel.IndexTarget) error {
44 | sql := fmt.Sprintf(`DROP INDEX "%s"`, name)
45 | _, err := ie.db.Exec(sql)
46 | return err
47 | }
48 |
--------------------------------------------------------------------------------
/database/postgres/pservice/index_getter.go:
--------------------------------------------------------------------------------
1 | package pservice
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/mrasu/GravityR/infra/postgres"
6 | "github.com/samber/lo"
7 | )
8 |
9 | type IndexGetter struct {
10 | db *postgres.DB
11 | }
12 |
13 | func NewIndexGetter(db *postgres.DB) *IndexGetter {
14 | return &IndexGetter{db: db}
15 | }
16 |
17 | func (ig *IndexGetter) GetIndexes(dbSchema string, tables []string) ([]*dmodel.IndexTarget, error) {
18 | infos, err := ig.db.GetIndexes(dbSchema, tables)
19 | if err != nil {
20 | return nil, err
21 | }
22 |
23 | return lo.Map(infos, func(info *postgres.IndexInfo, _ int) *dmodel.IndexTarget {
24 | return dmodel.NewIndexTarget(info.TableName, info.Columns)
25 | }), nil
26 | }
27 |
--------------------------------------------------------------------------------
/database/postgres/pservice/index_suggester.go:
--------------------------------------------------------------------------------
1 | package pservice
2 |
3 | import (
4 | "github.com/mrasu/GravityR/database/dmodel"
5 | "github.com/mrasu/GravityR/database/dservice"
6 | "github.com/mrasu/GravityR/database/postgres/pservice/parser"
7 | "github.com/mrasu/GravityR/infra/postgres"
8 | )
9 |
10 | type IndexSuggester struct {
11 | db *postgres.DB
12 | schema string
13 | }
14 |
15 | func NewIndexSuggester(db *postgres.DB, schema string) *IndexSuggester {
16 | return &IndexSuggester{
17 | db: db,
18 | schema: schema,
19 | }
20 | }
21 |
22 | func (is *IndexSuggester) Suggest(query string) ([]*dmodel.IndexTarget, error) {
23 | stmt, err := Parse(query)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | its, errs := parser.ListPossibleIndexes(is.db, is.schema, stmt)
29 | if len(errs) > 0 {
30 | return nil, errs[0]
31 | }
32 |
33 | its, err = is.removeExistingIndexTargets(is.db, is.schema, its)
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | return its, nil
39 | }
40 |
41 | func (is *IndexSuggester) removeExistingIndexTargets(db *postgres.DB, dbName string, its []*dmodel.IndexTarget) ([]*dmodel.IndexTarget, error) {
42 | idxGetter := NewIndexGetter(db)
43 | res, err := dservice.NewExistingIndexRemover(idxGetter, dbName, its).Remove()
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | return res, nil
49 | }
50 |
--------------------------------------------------------------------------------
/database/postgres/pservice/parse.go:
--------------------------------------------------------------------------------
1 | package pservice
2 |
3 | import (
4 | "github.com/auxten/postgresql-parser/pkg/sql/parser"
5 | "github.com/pkg/errors"
6 | )
7 |
8 | func Parse(query string) (*parser.Statement, error) {
9 | stmts, err := parser.Parse(query)
10 | if err != nil {
11 | return nil, errors.Wrap(err, "failed to parse sql")
12 | }
13 |
14 | if len(stmts) > 1 {
15 | return nil, errors.New("not supporting query having multiple statements")
16 | }
17 | return &stmts[0], nil
18 | }
19 |
--------------------------------------------------------------------------------
/database/postgres/pservice/parser/collect_table_schemas.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/database/dservice/dparser"
6 | "github.com/mrasu/GravityR/infra/postgres"
7 | "github.com/mrasu/GravityR/lib"
8 | "github.com/rs/zerolog/log"
9 | )
10 |
11 | func CollectTableSchemas(db *postgres.DB, schema string, tables []string) ([]*dparser.TableSchema, error) {
12 | cols, err := db.GetTableColumns(schema, tables)
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | schemas := dparser.CreateTableSchemas(tables, cols, func(c *postgres.ColumnInfo) (string, string, bool) {
18 | return c.TableName, c.ColumnName, c.IsPK
19 | })
20 |
21 | for i, table := range tables {
22 | if schemas[i] == nil {
23 | return nil, lib.NewUnsupportedError(fmt.Sprintf("unknown table found. perhaps using VIEW? not supporting: %s", table))
24 | }
25 | }
26 |
27 | log.Debug().Msg("Table schemas:")
28 | for i, s := range schemas {
29 | log.Printf("\t%d. %s", i, s.TableDescription())
30 | }
31 |
32 | return schemas, nil
33 | }
34 |
--------------------------------------------------------------------------------
/database/postgres/pservice/parser/index.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "github.com/auxten/postgresql-parser/pkg/sql/parser"
5 | "github.com/mrasu/GravityR/database/dmodel"
6 | "github.com/mrasu/GravityR/database/dservice/dparser"
7 | "github.com/mrasu/GravityR/infra/postgres"
8 | )
9 |
10 | func ListPossibleIndexes(db *postgres.DB, schema string, stmt *parser.Statement) ([]*dmodel.IndexTarget, []error) {
11 | tNames, errs := CollectTableNames(stmt)
12 | if len(errs) > 0 {
13 | return nil, errs
14 | }
15 |
16 | tables, err := CollectTableSchemas(db, schema, tNames)
17 | if err != nil {
18 | return nil, []error{err}
19 | }
20 |
21 | scopes, errs := CollectStmtScopes(stmt, "public")
22 | if len(errs) > 0 {
23 | return nil, errs
24 | }
25 |
26 | its, err := dparser.NewIndexTargetBuilder(tables).Build(scopes)
27 | if err != nil {
28 | return nil, []error{err}
29 | }
30 |
31 | return its, nil
32 | }
33 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | mysql8:
5 | image: mysql:8.0.23
6 | volumes:
7 | - ./docker/mysql8/data:/var/lib/mysql
8 | ports:
9 | - "3306:3306"
10 | environment:
11 | MYSQL_DATABASE: gravityr
12 | MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
13 | postgres14:
14 | profiles: [all]
15 | image: postgres:14
16 | volumes:
17 | - ./docker/postgres14/data:/var/lib/postgresql/data
18 | environment:
19 | POSTGRES_DB: gravityr
20 | POSTGRES_USER: root
21 | POSTGRES_HOST_AUTH_METHOD: trust
22 | # Uncomment to see show all queries
23 | # command: ["postgres", "-c", "log_statement=all"]
24 | ports:
25 | - "5432:5432"
26 | aws_mock:
27 | profiles: [all]
28 | build:
29 | context: ./docker/aws_mock
30 | volumes:
31 | - ./docker/aws_mock/fastapi:/app
32 | ports:
33 | - "8080:8080"
34 | jaeger_mock:
35 | profiles: [all]
36 | build:
37 | context: ./docker/jaeger_mock
38 | volumes:
39 | - ./docker/jaeger_mock/mock:/app
40 | ports:
41 | - "16685:16685"
42 | restart: unless-stopped
43 | hasura2:
44 | profiles: [all]
45 | image: hasura/graphql-engine:v2.10.0
46 | ports:
47 | - "8081:8080"
48 | depends_on:
49 | - postgres14
50 | restart: unless-stopped
51 | environment:
52 | HASURA_GRAPHQL_DATABASE_URL: postgres://root:@postgres14:5432/gravityr
53 | HASURA_GRAPHQL_ENABLE_CONSOLE: 'true'
54 | HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
55 | node:
56 | profiles: [all]
57 | build:
58 | context: ./docker/node
59 | command: ["tail", "-f", "/dev/null"]
60 | volumes:
61 | - ./docker/node/hasura:/hasura
62 | environment:
63 | HASURA_GRAPHQL_ENABLE_TELEMETRY: 'false'
64 | HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
65 |
66 |
--------------------------------------------------------------------------------
/docker/aws_mock/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10
2 |
3 | WORKDIR /app
4 | COPY ./fastapi/requirements.txt .
5 | RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
6 |
7 | CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8080"]
8 |
--------------------------------------------------------------------------------
/docker/aws_mock/fastapi/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, Header, Response, Request
2 | from starlette.responses import JSONResponse
3 |
4 | from pi import build_pi_get_resource_metrics_response
5 | from rds import build_rds_describe_db_instances_response
6 |
7 | app = FastAPI()
8 |
9 | @app.get("/")
10 | def read_root():
11 | return {"pong": "hello"}
12 |
13 | @app.post("/")
14 | async def read_aws(request: Request, content_type: str = Header(default="")):
15 | action = await get_action(request)
16 | if action == "PerformanceInsightsv20180227.GetResourceMetrics":
17 | return await build_pi_get_resource_metrics_response(request)
18 | elif action == "DescribeDBInstances":
19 | return build_rds_describe_db_instances_response()
20 | else:
21 | return build_unknown_action_response(content_type, action)
22 |
23 | async def get_action(request: Request) -> str:
24 | x_amz_target = request.headers.get("X-Amz-Target", "")
25 | form = await request.form()
26 | return form.get("Action", x_amz_target)
27 |
28 | def build_unknown_action_response(content_type: str, action: str) -> Response:
29 | if content_type == "application/x-www-form-urlencoded":
30 | content = f"""
31 |
32 |
33 | Sender
34 | UnknownAction
35 | Unknown Action: {action}
36 |
37 | 799fa280-3701-465f-8b26-49edcf814748
38 |
39 | """
40 | return Response(status_code=400, content=content, media_type="text/xml")
41 | else:
42 | detail = {"__type": "UnknownTargetException","Message": f"Unknown X-Amz-Target: {action}"}
43 | return JSONResponse(status_code=400, content=detail)
44 |
--------------------------------------------------------------------------------
/docker/aws_mock/fastapi/rds.py:
--------------------------------------------------------------------------------
1 | from fastapi import Response
2 |
3 | def build_rds_describe_db_instances_response() -> Response:
4 | with open("./data/rds/DescribeDbInstances.xml") as f:
5 | content = f.read()
6 | return Response(content=content, media_type="text/xml")
7 |
--------------------------------------------------------------------------------
/docker/aws_mock/fastapi/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | python-multipart
4 |
--------------------------------------------------------------------------------
/docker/go.mod:
--------------------------------------------------------------------------------
1 | module fake_go_module // Exclude this directory from Go tools
2 |
--------------------------------------------------------------------------------
/docker/jaeger_mock/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.19
2 | WORKDIR /app
3 | CMD ls
4 |
5 | COPY ./mock/go.mod .
6 | COPY ./mock/go.sum .
7 |
8 | RUN go mod download
9 |
10 | CMD ["go", "run", ".", "serve"]
11 |
--------------------------------------------------------------------------------
/docker/jaeger_mock/mock/mock_dependency_reader.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "github.com/jaegertracing/jaeger/model"
7 | "time"
8 | )
9 |
10 | type mockDependencyReader struct{}
11 |
12 | func (m *mockDependencyReader) GetDependencies(context.Context, time.Time, time.Duration) ([]model.DependencyLink, error) {
13 | return nil, errors.New("unexpected call: GetDependencies")
14 | }
15 |
--------------------------------------------------------------------------------
/docker/jaeger_mock/mock/mockdata/01_simple_trace.json:
--------------------------------------------------------------------------------
1 | {
2 | "Spans": [
3 | {
4 | "TraceID": "1",
5 | "SpanID": "1",
6 | "OperationName": "request",
7 | "StartTime": "2001-02-03T01:05:06Z",
8 | "DurationMilliSec": 1000,
9 | "Tags": {
10 | "foo": "bar"
11 | },
12 | "ServiceName": "bff",
13 | "SpanRef": {
14 | "TraceID": "1",
15 | "SpanID": "0000000000000000"
16 | }
17 | },
18 | {
19 | "TraceID": "1",
20 | "SpanID": "2",
21 | "OperationName": "getUser",
22 | "StartTime": "2001-02-03T01:05:06Z",
23 | "DurationMilliSec": 1000,
24 | "Tags": {
25 | "foo": "buz"
26 | },
27 | "ServiceName": "user",
28 | "SpanRef": {
29 | "TraceID": "1",
30 | "SpanID": "1"
31 | }
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/docker/jaeger_mock/mock/mockdata/02_slow_trace.json:
--------------------------------------------------------------------------------
1 | {
2 | "Spans": [
3 | {
4 | "TraceID": "2",
5 | "SpanID": "1",
6 | "OperationName": "request",
7 | "StartTime": "2001-02-03T02:05:06Z",
8 | "DurationMilliSec": 120000,
9 | "Tags": {
10 | "foo": "bar"
11 | },
12 | "ServiceName": "bff",
13 | "SpanRef": {
14 | "TraceID": "2",
15 | "SpanID": "0000000000000000"
16 | }
17 | },
18 | {
19 | "TraceID": "2",
20 | "SpanID": "2",
21 | "OperationName": "getUser",
22 | "StartTime": "2001-02-03T02:05:06Z",
23 | "DurationMilliSec": 120000,
24 | "Tags": {
25 | "foo": "buz"
26 | },
27 | "ServiceName": "user",
28 | "SpanRef": {
29 | "TraceID": "2",
30 | "SpanID": "1"
31 | }
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/docker/jaeger_mock/mock/mockdata/04_slow_trace_no_service.json:
--------------------------------------------------------------------------------
1 | {
2 | "Spans": [
3 | {
4 | "TraceID": "4",
5 | "SpanID": "1",
6 | "OperationName": "request",
7 | "StartTime": "2001-02-03T04:05:06Z",
8 | "DurationMilliSec": 120000,
9 | "Tags": {
10 | "foo": "bar"
11 | },
12 | "ServiceName": "bff",
13 | "SpanRef": {
14 | "TraceID": "4",
15 | "SpanID": "0000000000000000"
16 | }
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/docker/jaeger_mock/mock/mockdata/06_same_db_system.json:
--------------------------------------------------------------------------------
1 | {
2 | "Spans": [
3 | {
4 | "TraceID": "6",
5 | "SpanID": "1",
6 | "OperationName": "request",
7 | "StartTime": "2001-02-03T06:05:06Z",
8 | "DurationMilliSec": 1230000,
9 | "Tags": {
10 | "foo": "bar"
11 | },
12 | "ServiceName": "bff",
13 | "SpanRef": {
14 | "TraceID": "6",
15 | "SpanID": "0000000000000000"
16 | }
17 | },
18 | {
19 | "TraceID": "6",
20 | "SpanID": "2",
21 | "OperationName": "get user",
22 | "StartTime": "2001-02-03T06:05:06Z",
23 | "DurationMilliSec": 10000,
24 | "Tags": {
25 | "foo": "bar"
26 | },
27 | "ServiceName": "user",
28 | "SpanRef": {
29 | "TraceID": "6",
30 | "SpanID": "1"
31 | }
32 | },
33 | {
34 | "TraceID": "6",
35 | "SpanID": "3",
36 | "OperationName": "query",
37 | "StartTime": "2001-02-03T06:05:06Z",
38 | "DurationMilliSec": 10000,
39 | "Tags": {
40 | "db.system": "mysql"
41 | },
42 | "ServiceName": "user",
43 | "SpanRef": {
44 | "TraceID": "6",
45 | "SpanID": "2"
46 | }
47 | },
48 | {
49 | "TraceID": "6",
50 | "SpanID": "4",
51 | "OperationName": "get todo",
52 | "StartTime": "2001-02-03T06:05:16Z",
53 | "DurationMilliSec": 10000,
54 | "Tags": {
55 | "foo": "bar"
56 | },
57 | "ServiceName": "todo",
58 | "SpanRef": {
59 | "TraceID": "6",
60 | "SpanID": "1"
61 | }
62 | },
63 | {
64 | "TraceID": "6",
65 | "SpanID": "5",
66 | "OperationName": "get todo",
67 | "StartTime": "2001-02-03T06:05:16Z",
68 | "DurationMilliSec": 10000,
69 | "Tags": {
70 | "db.system": "mysql"
71 | },
72 | "ServiceName": "todo",
73 | "SpanRef": {
74 | "TraceID": "6",
75 | "SpanID": "4"
76 | }
77 | }
78 | ]
79 | }
80 |
--------------------------------------------------------------------------------
/docker/mysql8/init/script.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE users (
2 | id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
3 | name varchar(255) NOT NULL,
4 | email varchar(255) NOT NULL,
5 | password varchar(255) NOT NULL,
6 | created_at datetime DEFAULT NULL,
7 | updated_at datetime DEFAULT NULL
8 | );
9 |
10 | ALTER TABLE users ADD INDEX (id, email);
11 |
12 | CREATE TABLE tasks (
13 | id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
14 | user_id int NOT NULL,
15 | title varchar(50) NOT NULL,
16 | description varchar(100) DEFAULT NULL,
17 | status tinyint NOT NULL,
18 | created_at datetime DEFAULT NULL,
19 | updated_at datetime DEFAULT NULL
20 | );
21 |
22 | /* Insert 256*256(=65,536) rows */
23 | INSERT users(name, email, password, created_at, updated_at)
24 | WITH l0 AS (SELECT 0 AS num UNION ALL SELECT 1),
25 | l1 AS (SELECT a.num * 2 + b.num AS num FROM l0 AS a CROSS JOIN l0 AS b),
26 | l2 AS (SELECT a.num * 4 + b.num AS num FROM l1 AS a CROSS JOIN l1 AS b),
27 | l3 AS (SELECT a.num * 16 + b.num AS num FROM l2 AS a CROSS JOIN l2 AS b),
28 | l4 AS (SELECT a.num * 256 + b.num AS num FROM l3 AS a CROSS JOIN l3 AS b)
29 | SELECT CONCAT('test', CAST(num AS char)),
30 | CONCAT('test', CAST(num AS char), '@example.com'),
31 | 'p@ssw0rd',
32 | NOW() - INTERVAL ((num%363) + 1) DAY,
33 | NOW() - INTERVAL (num%363) DAY
34 | FROM l4;
35 |
36 | /* Insert 65,536*100(=6,553,600) rows */
37 | INSERT INTO tasks(user_id, title, description, status, created_at, updated_at)
38 | SELECT users.id,
39 | 'test title',
40 | 'test description',
41 | tmp_100.id % 3,
42 | users.updated_at - INTERVAL ((tmp_100.id%100) +5) HOUR,
43 | users.updated_at - INTERVAL (tmp_100.id%100) HOUR
44 | FROM users
45 | CROSS JOIN (SELECT * FROM users LIMIT 100) tmp_100;
46 |
--------------------------------------------------------------------------------
/docker/node/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16
2 |
3 | RUN npm install --location=global hasura-cli
4 |
--------------------------------------------------------------------------------
/docker/node/hasura/config.yaml:
--------------------------------------------------------------------------------
1 | version: 3
2 | endpoint: http://hasura2:8080
3 | metadata_directory: metadata
4 | actions:
5 | kind: synchronous
6 | handler_webhook_baseurl: http://localhost:3000
7 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/actions.graphql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrasu/GravityR/334f444ab5e0c8912b22d2bb7708484034551a2c/docker/node/hasura/metadata/actions.graphql
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/actions.yaml:
--------------------------------------------------------------------------------
1 | actions: []
2 | custom_types:
3 | enums: []
4 | input_objects: []
5 | objects: []
6 | scalars: []
7 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/allow_list.yaml:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/api_limits.yaml:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/cron_triggers.yaml:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/databases/databases.yaml:
--------------------------------------------------------------------------------
1 | - name: default
2 | kind: postgres
3 | configuration:
4 | connection_info:
5 | database_url:
6 | from_env: HASURA_GRAPHQL_DATABASE_URL
7 | isolation_level: read-committed
8 | pool_settings:
9 | connection_lifetime: 600
10 | idle_timeout: 180
11 | max_connections: 50
12 | retries: 1
13 | use_prepared_statements: true
14 | tables: "!include default/tables/tables.yaml"
15 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/databases/default/tables/public_tasks.yaml:
--------------------------------------------------------------------------------
1 | table:
2 | name: tasks
3 | schema: public
4 | object_relationships:
5 | - name: user
6 | using:
7 | manual_configuration:
8 | column_mapping:
9 | user_id: id
10 | insertion_order: null
11 | remote_table:
12 | name: users
13 | schema: public
14 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/databases/default/tables/public_users.yaml:
--------------------------------------------------------------------------------
1 | table:
2 | name: users
3 | schema: public
4 | array_relationships:
5 | - name: tasks
6 | using:
7 | manual_configuration:
8 | column_mapping:
9 | id: user_id
10 | insertion_order: null
11 | remote_table:
12 | name: tasks
13 | schema: public
14 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/databases/default/tables/tables.yaml:
--------------------------------------------------------------------------------
1 | - "!include public_tasks.yaml"
2 | - "!include public_users.yaml"
3 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/graphql_schema_introspection.yaml:
--------------------------------------------------------------------------------
1 | disabled_for_roles: []
2 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/inherited_roles.yaml:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/network.yaml:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/query_collections.yaml:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/remote_schemas.yaml:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/rest_endpoints.yaml:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/docker/node/hasura/metadata/version.yaml:
--------------------------------------------------------------------------------
1 | version: 3
2 |
--------------------------------------------------------------------------------
/docker/postgres14/init/script.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE users (
2 | id SERIAL PRIMARY KEY,
3 | name varchar(255) NOT NULL,
4 | email varchar(255) NOT NULL,
5 | password varchar(255) NOT NULL,
6 | created_at timestamp with time zone DEFAULT NULL,
7 | updated_at timestamp with time zone DEFAULT NULL
8 | );
9 |
10 | CREATE INDEX ON users(id, email);
11 |
12 | CREATE TABLE tasks (
13 | id SERIAL PRIMARY KEY,
14 | user_id int NOT NULL,
15 | title varchar(50) NOT NULL,
16 | description varchar(100) DEFAULT NULL,
17 | status smallint NOT NULL,
18 | created_at timestamp with time zone DEFAULT NULL,
19 | updated_at timestamp with time zone DEFAULT NULL
20 | );
21 |
22 | /* Insert 256*256(=65,536) rows */
23 | WITH l0 AS (SELECT 0 AS num UNION ALL SELECT 1),
24 | l1 AS (SELECT a.num * 2 + b.num AS num FROM l0 AS a CROSS JOIN l0 AS b),
25 | l2 AS (SELECT a.num * 4 + b.num AS num FROM l1 AS a CROSS JOIN l1 AS b),
26 | l3 AS (SELECT a.num * 16 + b.num AS num FROM l2 AS a CROSS JOIN l2 AS b),
27 | l4 AS (SELECT a.num * 256 + b.num AS num FROM l3 AS a CROSS JOIN l3 AS b)
28 | INSERT INTO users(name, email, password, created_at, updated_at)
29 | SELECT CONCAT('test', num),
30 | CONCAT('test', num, '@example.com'),
31 | 'p@ssw0rd',
32 | CURRENT_TIMESTAMP - INTERVAL '1 DAY' * ((num%363) + 1),
33 | CURRENT_TIMESTAMP - INTERVAL '1 DAY' * (num%363)
34 | FROM l4;
35 |
36 | /* Insert 65,536*100(=6,553,600) rows */
37 | INSERT INTO tasks(user_id, title, description, status, created_at, updated_at)
38 | SELECT users.id,
39 | 'test title',
40 | 'test description',
41 | tmp_100.id % 3,
42 | users.updated_at - INTERVAL '1 HOUR' * ((tmp_100.id%100) +5),
43 | users.updated_at - INTERVAL '1 HOUR' * (tmp_100.id%100)
44 | FROM users
45 | CROSS JOIN (SELECT * FROM users LIMIT 100) tmp_100;
46 |
--------------------------------------------------------------------------------
/docs/images/jaeger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrasu/GravityR/334f444ab5e0c8912b22d2bb7708484034551a2c/docs/images/jaeger.png
--------------------------------------------------------------------------------
/docs/images/postgres.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrasu/GravityR/334f444ab5e0c8912b22d2bb7708484034551a2c/docs/images/postgres.png
--------------------------------------------------------------------------------
/html/html_builder.go:
--------------------------------------------------------------------------------
1 | package html
2 |
3 | import (
4 | "github.com/mrasu/GravityR/injection"
5 | "github.com/pkg/errors"
6 | "html/template"
7 | "io/fs"
8 | "os"
9 | )
10 |
11 | const (
12 | TypeMain = "main"
13 | TypeMermaid = "mermaid"
14 | )
15 |
16 | func CreateHtml(htmlType, filename string, bo *BuildOption) error {
17 | script, err := fs.ReadFile(injection.ClientDist, "client/dist/assets/"+htmlType+".js")
18 | if err != nil {
19 | return errors.Wrap(err, "failed to open file")
20 | }
21 | style, err := fs.ReadFile(injection.ClientDist, "client/dist/assets/"+htmlType+".css")
22 | if err != nil {
23 | return errors.Wrap(err, "failed to open file")
24 | }
25 |
26 | tmpl := `
27 |
28 |
29 |
30 |
31 |
32 | GravityR
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | `
44 | tpl, err := template.New("").Parse(tmpl)
45 | if err != nil {
46 | return errors.Wrap(err, "failed create template")
47 | }
48 | m := map[string]interface{}{
49 | "script": template.JS(script),
50 | "style": template.CSS(style),
51 | "gr": bo.createGrMap(),
52 | }
53 |
54 | f, err := os.Create(filename)
55 | if err != nil {
56 | return errors.Wrap(err, "failed to create html file")
57 | }
58 |
59 | err = tpl.Execute(f, m)
60 | if err != nil {
61 | return errors.Wrap(err, "failed to write template to html")
62 | }
63 |
64 | return nil
65 | }
66 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_db_load.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | type VmDbLoad struct {
4 | Name string `json:"name"`
5 | Sqls []*VmDbLoadOfSql `json:"sqls"`
6 | }
7 |
8 | func NewVmDbLoad(name string) *VmDbLoad {
9 | return &VmDbLoad{
10 | Name: name,
11 | Sqls: []*VmDbLoadOfSql{},
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_db_load_of_sql.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | type VmDbLoadOfSql struct {
4 | Sql string `json:"sql"`
5 | LoadMax float64 `json:"loadMax"`
6 | LoadSum float64 `json:"loadSum"`
7 | TokenizedId string `json:"tokenizedId"`
8 | }
9 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_examination_command_option.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | import (
4 | "path/filepath"
5 | "strings"
6 | )
7 |
8 | type VmExaminationCommandOption struct {
9 | IsShort bool `json:"isShort"`
10 | Name string `json:"name"`
11 | Value string `json:"value"`
12 | }
13 |
14 | func CreateOutputExaminationOption(addPrefix bool, filename string) *VmExaminationCommandOption {
15 | val := filename
16 | if addPrefix {
17 | val = strings.TrimSuffix(filename, filepath.Ext(filename)) + "_examine" + filepath.Ext(filename)
18 | }
19 |
20 | return &VmExaminationCommandOption{
21 | IsShort: true,
22 | Name: "o",
23 | Value: val,
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_examination_index_result.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | type VmExaminationIndexResult struct {
4 | ExecutionTimeMillis int64 `json:"executionTimeMillis"`
5 | IndexTarget *VmIndexTarget `json:"indexTarget"`
6 | }
7 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_examination_result.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | type VmExaminationResult struct {
4 | OriginalTimeMillis int64 `json:"originalTimeMillis"`
5 | IndexResults []*VmExaminationIndexResult `json:"indexResults"`
6 | }
7 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_index_column.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | type VmIndexColumn struct {
4 | Name string `json:"name"`
5 | }
6 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_index_target.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | type VmIndexTarget struct {
4 | TableName string `json:"tableName"`
5 | Columns []*VmIndexColumn `json:"columns"`
6 | }
7 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_mysql_explain_analyze_node.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | import "gopkg.in/guregu/null.v4"
4 |
5 | type VmMysqlExplainAnalyzeNode struct {
6 | Text string `json:"text"`
7 | Title string `json:"title"`
8 | TableName string `json:"tableName"`
9 | EstimatedInitCost null.Float `json:"estimatedInitCost"`
10 | EstimatedCost null.Float `json:"estimatedCost"`
11 | EstimatedReturnedRows null.Int `json:"estimatedReturnedRows"`
12 | ActualTimeFirstRow null.Float `json:"actualTimeFirstRow"`
13 | ActualTimeAvg null.Float `json:"actualTimeAvg"`
14 | ActualReturnedRows null.Int `json:"actualReturnedRows"`
15 | ActualLoopCount null.Int `json:"actualLoopCount"`
16 |
17 | Children []*VmMysqlExplainAnalyzeNode `json:"children"`
18 | }
19 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_otel_compact_trace.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | type VmOtelCompactTrace struct {
4 | TraceId string `json:"traceId"`
5 | SameServiceAccessCount int `json:"sameServiceAccessCount"`
6 | TimeConsumingServiceName string `json:"timeConsumingServiceName"`
7 | Root *VmOtelTraceSpan `json:"root"`
8 | }
9 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_otel_trace_span.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | type VmOtelTraceSpan struct {
4 | Name string `json:"name"`
5 | SpanId string `json:"spanId"`
6 | StartTimeMillis int `json:"startTimeMillis"`
7 | EndTimeMillis int `json:"endTimeMillis"`
8 | ServiceName string `json:"serviceName"`
9 | Children []*VmOtelTraceSpan `json:"children"`
10 | }
11 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_postgres_explain_analyze_node.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | import "gopkg.in/guregu/null.v4"
4 |
5 | type VmPostgresExplainAnalyzeNode struct {
6 | Text string `json:"text"`
7 | Title string `json:"title"`
8 | TableName string `json:"tableName"`
9 | EstimatedInitCost null.Float `json:"estimatedInitCost"`
10 | EstimatedCost null.Float `json:"estimatedCost"`
11 | EstimatedReturnedRows null.Int `json:"estimatedReturnedRows"`
12 | EstimatedWidth null.Int `json:"estimatedWidth"`
13 | ActualTimeFirstRow null.Float `json:"actualTimeFirstRow"`
14 | ActualTimeAvg null.Float `json:"actualTimeAvg"`
15 | ActualReturnedRows null.Int `json:"actualReturnedRows"`
16 | ActualLoopCount null.Int `json:"actualLoopCount"`
17 |
18 | Children []*VmPostgresExplainAnalyzeNode `json:"children"`
19 | }
20 |
--------------------------------------------------------------------------------
/html/viewmodel/vm_time_db_load.go:
--------------------------------------------------------------------------------
1 | package viewmodel
2 |
3 | import (
4 | "strconv"
5 | "time"
6 | )
7 |
8 | type VmTimestamp time.Time
9 |
10 | func (t VmTimestamp) MarshalJSON() ([]byte, error) {
11 | return []byte(strconv.FormatInt(time.Time(t).UnixMilli(), 10)), nil
12 | }
13 |
14 | type VmTimeDbLoad struct {
15 | Timestamp VmTimestamp `json:"timestamp"`
16 | Databases []*VmDbLoad `json:"databases"`
17 | }
18 |
19 | func NewVmTimeDbLoad(t time.Time) *VmTimeDbLoad {
20 | return &VmTimeDbLoad{
21 | Timestamp: VmTimestamp(t),
22 | Databases: []*VmDbLoad{},
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/infra/aws/aws.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "context"
5 | "github.com/aws/aws-sdk-go-v2/aws"
6 | "github.com/aws/aws-sdk-go-v2/config"
7 | "github.com/aws/smithy-go/logging"
8 | "github.com/pkg/errors"
9 | "os"
10 | )
11 |
12 | const mockEndpoint = "http://localhost:8080"
13 | const verboseMode = aws.LogRetries | aws.LogRequest | aws.LogResponse
14 |
15 | func NewAwsConfig(useMock bool, verbose bool) (aws.Config, error) {
16 | if useMock {
17 | return newMockedConfig(verbose), nil
18 | }
19 |
20 | var optFns []func(*config.LoadOptions) error
21 | if verbose {
22 | optFns = append(optFns, config.WithClientLogMode(verboseMode))
23 | }
24 |
25 | cfg, err := config.LoadDefaultConfig(context.Background(), optFns...)
26 | if err != nil {
27 | return cfg, errors.Wrap(err, "failed to load aws config")
28 | }
29 |
30 | return cfg, nil
31 | }
32 |
33 | func newMockedConfig(verbose bool) aws.Config {
34 | cfg := aws.Config{
35 | Logger: logging.NewStandardLogger(os.Stderr),
36 | }
37 |
38 | cfg.EndpointResolverWithOptions = aws.EndpointResolverWithOptionsFunc(
39 | func(service, region string, options ...interface{}) (aws.Endpoint, error) {
40 | return aws.Endpoint{
41 | URL: mockEndpoint,
42 | }, nil
43 | },
44 | )
45 | if verbose {
46 | cfg.ClientLogMode = verboseMode
47 | }
48 |
49 | return cfg
50 | }
51 |
--------------------------------------------------------------------------------
/infra/aws/pi_sql_load_avg.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "github.com/mrasu/GravityR/html/viewmodel"
5 | "github.com/mrasu/GravityR/lib"
6 | "time"
7 | )
8 |
9 | type PiSQLLoadAvg struct {
10 | DbName string
11 | SQL string
12 | TokenizedId string
13 | Values []*PiTimeValue
14 | }
15 |
16 | func NewPiSQLLoadAvg(dbName, sql, tokenizedId string) *PiSQLLoadAvg {
17 | return &PiSQLLoadAvg{
18 | DbName: dbName,
19 | SQL: sql,
20 | TokenizedId: tokenizedId,
21 | }
22 | }
23 |
24 | func ConvertPiSQLLoadAvgsToVms(start, end time.Time, avgs []*PiSQLLoadAvg) []*viewmodel.VmTimeDbLoad {
25 | sqlLoadMap := map[time.Time]*viewmodel.VmTimeDbLoad{}
26 | dbLoadMap := map[time.Time]map[string]*viewmodel.VmDbLoad{}
27 | for _, avg := range avgs {
28 | for _, v := range avg.Values {
29 | baseTime := lib.NormalizeTimeByHour(v.Time)
30 | if _, ok := sqlLoadMap[baseTime]; !ok {
31 | sqlLoadMap[baseTime] = viewmodel.NewVmTimeDbLoad(baseTime)
32 | }
33 | if _, ok := dbLoadMap[baseTime]; !ok {
34 | dbLoadMap[baseTime] = map[string]*viewmodel.VmDbLoad{}
35 | }
36 | load := sqlLoadMap[baseTime]
37 |
38 | if _, ok := dbLoadMap[baseTime][avg.DbName]; !ok {
39 | dbLoad := viewmodel.NewVmDbLoad(avg.DbName)
40 | dbLoadMap[baseTime][avg.DbName] = dbLoad
41 | load.Databases = append(load.Databases, dbLoad)
42 | }
43 | dbLoad := dbLoadMap[baseTime][avg.DbName]
44 |
45 | found := false
46 | for _, loadOfSql := range dbLoad.Sqls {
47 | if loadOfSql.Sql == avg.SQL {
48 | if loadOfSql.LoadMax < v.Value {
49 | loadOfSql.LoadMax = v.Value
50 | }
51 | loadOfSql.LoadSum += v.Value
52 |
53 | found = true
54 | break
55 | }
56 | }
57 |
58 | if !found {
59 | dbLoad.Sqls = append(dbLoad.Sqls, &viewmodel.VmDbLoadOfSql{
60 | Sql: avg.SQL,
61 | LoadMax: v.Value,
62 | LoadSum: v.Value,
63 | TokenizedId: avg.TokenizedId,
64 | })
65 | }
66 | }
67 | }
68 |
69 | target := lib.NormalizeTimeByHour(start).UTC()
70 | var res []*viewmodel.VmTimeDbLoad
71 | for !target.After(end) {
72 | if l, ok := sqlLoadMap[target]; ok {
73 | res = append(res, l)
74 | } else {
75 | res = append(res, viewmodel.NewVmTimeDbLoad(target))
76 | }
77 | target = target.Add(1 * time.Hour)
78 | }
79 |
80 | return res
81 | }
82 |
--------------------------------------------------------------------------------
/infra/aws/pi_time_value.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import "time"
4 |
5 | type PiTimeValue struct {
6 | Time time.Time
7 | Value float64
8 | }
9 |
--------------------------------------------------------------------------------
/infra/aws/rds.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "context"
5 | "github.com/aws/aws-sdk-go-v2/aws"
6 | aRds "github.com/aws/aws-sdk-go-v2/service/rds"
7 | "github.com/aws/aws-sdk-go-v2/service/rds/types"
8 | "github.com/mrasu/GravityR/lib"
9 | "github.com/pkg/errors"
10 | )
11 |
12 | type Rds struct {
13 | client aRds.DescribeDBInstancesAPIClient
14 | }
15 |
16 | func NewRds(cfg aws.Config) *Rds {
17 | cli := aRds.NewFromConfig(cfg)
18 | return NewRdsWithCli(cli)
19 | }
20 |
21 | func NewRdsWithCli(cli aRds.DescribeDBInstancesAPIClient) *Rds {
22 | return &Rds{
23 | client: cli,
24 | }
25 | }
26 |
27 | func (rds *Rds) GetDBs(engines []string) ([]*RdsDB, error) {
28 | output, err := rds.client.DescribeDBInstances(context.Background(), &aRds.DescribeDBInstancesInput{
29 | Filters: []types.Filter{
30 | {Name: lib.Ptr("engine"), Values: engines},
31 | },
32 | })
33 | if err != nil {
34 | return nil, errors.Wrap(err, "failed to describe db instances")
35 | }
36 |
37 | var res []*RdsDB
38 | for _, ins := range output.DBInstances {
39 | res = append(res, &RdsDB{
40 | InstanceIdentifier: *ins.DBInstanceIdentifier,
41 | DbiResourceId: *ins.DbiResourceId,
42 | })
43 | }
44 |
45 | return res, nil
46 | }
47 |
--------------------------------------------------------------------------------
/infra/aws/rds_db.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | type RdsDB struct {
4 | InstanceIdentifier string
5 | DbiResourceId string
6 | }
7 |
--------------------------------------------------------------------------------
/infra/aws/rds_test.go:
--------------------------------------------------------------------------------
1 | package aws_test
2 |
3 | import (
4 | aAws "github.com/aws/aws-sdk-go-v2/aws"
5 | "github.com/jarcoal/httpmock"
6 | "github.com/mrasu/GravityR/infra/aws"
7 | "github.com/mrasu/GravityR/thelper"
8 | "github.com/stretchr/testify/assert"
9 | "net/http"
10 | "net/url"
11 | "testing"
12 | )
13 |
14 | func TestRds_GetDBs(t *testing.T) {
15 | httpmock.Activate()
16 | defer httpmock.DeactivateAndReset()
17 |
18 | expDBs := []*aws.RdsDB{
19 | {
20 | InstanceIdentifier: "gravityr1",
21 | DbiResourceId: "db-XXXXXXXXXXXXXXXXXXXXXXXXX1",
22 | },
23 | {
24 | InstanceIdentifier: "gravityr2",
25 | DbiResourceId: "db-XXXXXXXXXXXXXXXXXXXXXXXXX2",
26 | },
27 | }
28 | expReqBody := map[string][]string{
29 | "Action": {"DescribeDBInstances"},
30 | "Filters.Filter.1.Name": {"engine"},
31 | "Filters.Filter.1.Values.Value.1": {"mysql"},
32 | "Version": {"2014-10-31"},
33 | }
34 |
35 | reqBody := map[string][]string{}
36 | httpmock.RegisterResponder("POST", "https://rds.ap-northeast-1.amazonaws.com/",
37 | func(req *http.Request) (*http.Response, error) {
38 | reqBody = unmarshalRdsBody(t, req)
39 |
40 | resp := thelper.ReadFromFiles(t, "aws/rds/DescribeDbInstances.xml")
41 | return httpmock.NewStringResponse(200, resp), nil
42 | },
43 | )
44 |
45 | rds := buildRds(t)
46 | dbs, err := rds.GetDBs([]string{"mysql"})
47 | assert.NoError(t, err)
48 |
49 | assert.Equal(t, 1, httpmock.GetTotalCallCount())
50 | assert.Equal(t, expReqBody, reqBody)
51 | assert.Equal(t, expDBs, dbs)
52 | }
53 |
54 | func buildRds(t *testing.T) *aws.Rds {
55 | t.Helper()
56 |
57 | cfg := aAws.Config{
58 | Region: "ap-northeast-1",
59 | }
60 | cfg.HTTPClient = http.DefaultClient
61 |
62 | return aws.NewRds(cfg)
63 | }
64 |
65 | func unmarshalRdsBody(t *testing.T, req *http.Request) url.Values {
66 | t.Helper()
67 |
68 | err := req.ParseForm()
69 | assert.NoError(t, err)
70 |
71 | return req.Form
72 | }
73 |
--------------------------------------------------------------------------------
/infra/hasura/column_info.go:
--------------------------------------------------------------------------------
1 | package hasura
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/lib"
6 | "github.com/pkg/errors"
7 | "regexp"
8 | "strings"
9 | )
10 |
11 | const columnFetchQuery = `
12 | SELECT
13 | pg_class.relname AS table_name,
14 | pg_attribute.attname AS column_name,
15 | COALESCE(pg_index.indisprimary, FALSE) AS is_pk
16 | FROM
17 | pg_class
18 | INNER JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid
19 | INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
20 | LEFT OUTER JOIN pg_index ON pg_class.oid = pg_index.indrelid AND pg_attribute.attnum = ANY(pg_index.indkey)
21 | WHERE
22 | pg_namespace.nspname = ? AND
23 | pg_class.relname IN (?) AND
24 | pg_class.relkind = 'r' AND
25 | pg_attribute.attnum >= 0
26 | ORDER BY
27 | pg_class.relname,
28 | pg_attribute.attnum
29 | `
30 |
31 | type ColumnInfo struct {
32 | ColumnName string
33 | IsPK bool
34 | TableName string
35 | }
36 |
37 | var wordOnlyReg = regexp.MustCompile(`\A\w+\z`)
38 |
39 | func buildColumnFetchQuery(schema string, tableNames []string) (string, error) {
40 | if !wordOnlyReg.MatchString(schema) {
41 | return "", errors.Errorf("schema name can contains only a-z or _: %s", schema)
42 | }
43 | for _, t := range tableNames {
44 | if !wordOnlyReg.MatchString(t) {
45 | return "", errors.Errorf("table name can contains only a-z or _: %s", t)
46 | }
47 | }
48 | ts := lib.Join(tableNames, ",", func(v string) string { return fmt.Sprintf("'%s'", v) })
49 | q := strings.Replace(columnFetchQuery, "?", fmt.Sprintf("'%s'", schema), 1)
50 | q = strings.Replace(q, "?", ts, 1)
51 |
52 | return q, nil
53 | }
54 |
55 | func parseColumnFetchResult(res *RunSQLResponse) []*ColumnInfo {
56 | var cols []*ColumnInfo
57 | for i, r := range res.Result {
58 | if i == 0 {
59 | // first row shows name of columns
60 | continue
61 | }
62 |
63 | isPk := false
64 | if r[2] == "t" {
65 | isPk = true
66 | }
67 |
68 | cols = append(cols, &ColumnInfo{
69 | TableName: r[0],
70 | ColumnName: r[1],
71 | IsPK: isPk,
72 | })
73 | }
74 |
75 | return cols
76 | }
77 |
--------------------------------------------------------------------------------
/infra/hasura/config.go:
--------------------------------------------------------------------------------
1 | package hasura
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/lib"
6 | "github.com/pkg/errors"
7 | "net/url"
8 | )
9 |
10 | type Config struct {
11 | Url *url.URL
12 | AdminSecret string
13 | }
14 |
15 | func NewConfigFromEnv() (*Config, error) {
16 | u, err := lib.GetEnv("HASURA_URL")
17 | if err != nil {
18 | return nil, err
19 | }
20 | uu, err := url.Parse(u)
21 | if err != nil {
22 | return nil, errors.Wrap(err, fmt.Sprintf("cannot parse URL for Hasura: %s", u))
23 | }
24 |
25 | s, err := lib.GetEnv("HASURA_ADMIN_SECRET")
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | return &Config{
31 | Url: uu,
32 | AdminSecret: s,
33 | }, nil
34 | }
35 |
--------------------------------------------------------------------------------
/infra/hasura/explain_request_body.go:
--------------------------------------------------------------------------------
1 | package hasura
2 |
3 | type ExplainRequestBody struct {
4 | Query *Query `json:"query"`
5 | User map[string]string `json:"user"`
6 | }
7 |
--------------------------------------------------------------------------------
/infra/hasura/explain_response.go:
--------------------------------------------------------------------------------
1 | package hasura
2 |
3 | type ExplainResponse struct {
4 | Field string `json:"field"`
5 | Plan []string `json:"plan"`
6 | SQL string `json:"sql"`
7 | }
8 |
--------------------------------------------------------------------------------
/infra/hasura/index_info.go:
--------------------------------------------------------------------------------
1 | package hasura
2 |
3 | import (
4 | "fmt"
5 | "github.com/lib/pq"
6 | "github.com/mrasu/GravityR/lib"
7 | "github.com/pkg/errors"
8 | "strings"
9 | )
10 |
11 | const indexFetchQuery = `
12 | SELECT
13 | t.relname AS table_name,
14 | ARRAY(
15 | SELECT PG_GET_INDEXDEF(pg_index.indexrelid, k + 1, TRUE)
16 | FROM GENERATE_SUBSCRIPTS(pg_index.indkey, 1) AS k
17 | ORDER BY k
18 | ) AS column_names
19 | FROM pg_index
20 | INNER JOIN pg_class AS i ON pg_index.indexrelid = i.oid
21 | INNER JOIN pg_class AS t ON pg_index.indrelid = t.oid
22 | INNER JOIN pg_namespace ON i.relnamespace = pg_namespace.oid
23 | WHERE
24 | pg_namespace.nspname = ? AND
25 | t.relname IN (?)
26 | ORDER BY
27 | t.relname,
28 | i.relname
29 | `
30 |
31 | type IndexInfo struct {
32 | TableName string
33 | Columns []string
34 | }
35 |
36 | func buildIndexFetchQuery(schema string, tableNames []string) (string, error) {
37 | if !wordOnlyReg.MatchString(schema) {
38 | return "", errors.Errorf("schema name can contains only a-z or _: %s", schema)
39 | }
40 | for _, t := range tableNames {
41 | if !wordOnlyReg.MatchString(t) {
42 | return "", errors.Errorf("table name can contains only a-z or _: %s", t)
43 | }
44 | }
45 | ts := lib.Join(tableNames, ",", func(v string) string { return fmt.Sprintf("'%s'", v) })
46 | q := strings.Replace(indexFetchQuery, "?", fmt.Sprintf("'%s'", schema), 1)
47 | q = strings.Replace(q, "?", ts, 1)
48 |
49 | return q, nil
50 | }
51 |
52 | func parseIndexFetchResult(res *RunSQLResponse) ([]*IndexInfo, error) {
53 | var infos []*IndexInfo
54 | for i, r := range res.Result {
55 | if i == 0 {
56 | // first row shows name of columns
57 | continue
58 | }
59 |
60 | cols := pq.StringArray{}
61 | err := cols.Scan(r[1])
62 | if err != nil {
63 | return nil, errors.Wrap(err, "unexpected result from hasura to fetch indexes: not array of string for column names")
64 | }
65 |
66 | infos = append(infos, &IndexInfo{
67 | TableName: r[0],
68 | Columns: cols,
69 | })
70 | }
71 |
72 | return infos, nil
73 | }
74 |
--------------------------------------------------------------------------------
/infra/hasura/query.go:
--------------------------------------------------------------------------------
1 | package hasura
2 |
3 | type Query struct {
4 | Query string `json:"query"`
5 | Variables map[string]interface{} `json:"variables"`
6 | }
7 |
--------------------------------------------------------------------------------
/infra/hasura/run_sql_args.go:
--------------------------------------------------------------------------------
1 | package hasura
2 |
3 | type RunSQLArgs struct {
4 | SQL string `json:"sql"`
5 | Source *string `json:"source"`
6 | Cascade *bool `json:"cascade"`
7 | CheckMetadataConsistency *bool `json:"check_metadata_consistency"`
8 | ReadOnly *bool `json:"read_only"`
9 | }
10 |
--------------------------------------------------------------------------------
/infra/hasura/run_sql_response.go:
--------------------------------------------------------------------------------
1 | package hasura
2 |
3 | type RunSQLResponse struct {
4 | ResultType string `json:"result_type"`
5 | Result [][]string `json:"result"`
6 | }
7 |
--------------------------------------------------------------------------------
/infra/jaeger/client.go:
--------------------------------------------------------------------------------
1 | package jaeger
2 |
3 | import (
4 | "context"
5 | "github.com/jaegertracing/jaeger/proto-gen/api_v3"
6 | "github.com/pkg/errors"
7 | "google.golang.org/grpc"
8 | "google.golang.org/grpc/credentials/insecure"
9 | "io"
10 | "time"
11 | )
12 |
13 | type Client struct {
14 | conn *grpc.ClientConn
15 | }
16 |
17 | func Open(address string, isSecure bool) (*Client, error) {
18 | opts := []grpc.DialOption{
19 | grpc.WithBlock(),
20 | }
21 | if !isSecure {
22 | opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
23 | }
24 |
25 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
26 | defer cancel()
27 | conn, err := grpc.DialContext(ctx, address, opts...)
28 | if err != nil {
29 | return nil, errors.Wrapf(err, "cannot connect to %s", address)
30 | }
31 |
32 | return NewClient(conn), nil
33 | }
34 |
35 | func NewClient(conn *grpc.ClientConn) *Client {
36 | return &Client{conn: conn}
37 | }
38 |
39 | func (c *Client) Close() error {
40 | err := c.conn.Close()
41 | if err != nil {
42 | return errors.Wrap(err, "failed to close grpc connection")
43 | }
44 | return nil
45 | }
46 |
47 | func (c *Client) FindTraces(param *api_v3.TraceQueryParameters) ([]*Trace, error) {
48 | client := api_v3.NewQueryServiceClient(c.conn)
49 | ctx := context.Background()
50 | req := &api_v3.FindTracesRequest{
51 | Query: param,
52 | }
53 | resp, err := client.FindTraces(ctx, req)
54 | if err != nil {
55 | return nil, errors.Wrap(err, "failed to find traces")
56 | }
57 |
58 | var traces []*Trace
59 | for {
60 | chunk, err := resp.Recv()
61 | if err != nil {
62 | if errors.Is(err, io.EOF) {
63 | break
64 | }
65 | return nil, errors.Wrap(err, "failed to receive result to find traces")
66 | }
67 | spans := chunk.GetResourceSpans()
68 |
69 | traces = append(traces, &Trace{Spans: spans})
70 | }
71 |
72 | return traces, nil
73 | }
74 |
--------------------------------------------------------------------------------
/infra/jaeger/trace.go:
--------------------------------------------------------------------------------
1 | package jaeger
2 |
3 | import (
4 | v1Trace "github.com/jaegertracing/jaeger/proto-gen/otel/trace/v1"
5 | )
6 |
7 | type Trace struct {
8 | Spans []*v1Trace.ResourceSpans
9 | }
10 |
--------------------------------------------------------------------------------
/infra/mysql/column_info.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | const columnFetchQuery = `
4 | SELECT
5 | COLUMN_NAME,
6 | COLUMN_KEY,
7 | TABLE_NAME
8 | FROM
9 | information_schema.columns
10 | WHERE
11 | TABLE_SCHEMA = ? AND
12 | TABLE_NAME IN (?)
13 | ORDER BY TABLE_NAME, ORDINAL_POSITION
14 | `
15 |
16 | type ColumnInfo struct {
17 | ColumnName string `db:"COLUMN_NAME"`
18 | ColumnKey string `db:"COLUMN_KEY"`
19 | TableName string `db:"TABLE_NAME"`
20 | }
21 |
--------------------------------------------------------------------------------
/infra/mysql/config.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "github.com/go-sql-driver/mysql"
5 | "github.com/mrasu/GravityR/lib"
6 | "os"
7 | )
8 |
9 | type Config struct {
10 | config *mysql.Config
11 | paramText string
12 | }
13 |
14 | func NewConfigFromEnv() (*Config, error) {
15 | cfg := mysql.NewConfig()
16 | username, err := lib.GetEnv("DB_USERNAME")
17 | if err != nil {
18 | return nil, err
19 | }
20 | cfg.User = username
21 |
22 | cfg.Passwd = os.Getenv("DB_PASSWORD")
23 | protocol := os.Getenv("DB_PROTOCOL")
24 | if len(protocol) == 0 {
25 | protocol = "tcp"
26 | }
27 | cfg.Net = protocol
28 |
29 | address := os.Getenv("DB_ADDRESS")
30 | if len(address) > 0 {
31 | cfg.Addr = address
32 | }
33 |
34 | database, err := lib.GetEnv("DB_DATABASE")
35 | if err != nil {
36 | return nil, err
37 | }
38 | cfg.DBName = database
39 |
40 | paramText := os.Getenv("DB_PARAM_TEXT")
41 |
42 | return &Config{
43 | config: cfg,
44 | paramText: paramText,
45 | }, nil
46 | }
47 |
48 | func (cfg *Config) ToDSN() string {
49 | dsn := cfg.config.FormatDSN()
50 | if len(cfg.paramText) > 0 {
51 | dsn += "?" + cfg.paramText
52 | }
53 |
54 | return dsn
55 | }
56 |
57 | func (cfg *Config) GetDBName() string {
58 | return cfg.config.DBName
59 | }
60 |
--------------------------------------------------------------------------------
/infra/mysql/index_info.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | const indexFetchQuery = `
4 | SELECT
5 | TABLE_NAME,
6 | INDEX_NAME,
7 | COLUMN_NAME
8 | FROM
9 | information_schema.STATISTICS
10 | WHERE
11 | TABLE_SCHEMA = ? AND
12 | TABLE_NAME IN (?)
13 | ORDER BY
14 | TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
15 | `
16 |
17 | type IndexInfo struct {
18 | TableName string
19 | Columns []string
20 | }
21 |
22 | type flatIndexInfo struct {
23 | TableName string `db:"TABLE_NAME"`
24 | IndexName string `db:"INDEX_NAME"`
25 | ColumnName string `db:"COLUMN_NAME"`
26 | }
27 |
28 | func toIndexInfo(orderedFlatInfo []*flatIndexInfo) []*IndexInfo {
29 | var infos []*IndexInfo
30 |
31 | lastTName := ""
32 | lastIName := ""
33 | for _, flatInfo := range orderedFlatInfo {
34 | if lastTName == flatInfo.TableName && lastIName == flatInfo.IndexName {
35 | idx := infos[len(infos)-1]
36 | idx.Columns = append(idx.Columns, flatInfo.ColumnName)
37 | } else {
38 | infos = append(infos, &IndexInfo{
39 | TableName: flatInfo.TableName,
40 | Columns: []string{flatInfo.ColumnName},
41 | })
42 | }
43 |
44 | lastTName = flatInfo.TableName
45 | lastIName = flatInfo.IndexName
46 | }
47 |
48 | return infos
49 | }
50 |
--------------------------------------------------------------------------------
/infra/postgres/column_info.go:
--------------------------------------------------------------------------------
1 | package postgres
2 |
3 | const columnFetchQuery = `
4 | SELECT
5 | pg_class.relname AS table_name,
6 | pg_attribute.attname AS column_name,
7 | COALESCE(pg_index.indisprimary, FALSE) AS is_pk
8 | FROM
9 | pg_class
10 | INNER JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid
11 | INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
12 | LEFT OUTER JOIN pg_index ON pg_class.oid = pg_index.indrelid AND pg_attribute.attnum = ANY(pg_index.indkey)
13 | WHERE
14 | pg_namespace.nspname = ? AND
15 | pg_class.relname IN (?) AND
16 | pg_class.relkind = 'r' AND
17 | pg_attribute.attnum >= 0
18 | ORDER BY
19 | pg_class.relname,
20 | pg_attribute.attnum;
21 | `
22 |
23 | type ColumnInfo struct {
24 | ColumnName string `db:"column_name"`
25 | IsPK bool `db:"is_pk"`
26 | TableName string `db:"table_name"`
27 | }
28 |
--------------------------------------------------------------------------------
/infra/postgres/config.go:
--------------------------------------------------------------------------------
1 | package postgres
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "os"
6 | "strings"
7 | )
8 |
9 | type Config struct {
10 | user string
11 | password string
12 | host string
13 | port string
14 | DBName string
15 | searchPath string
16 | paramText string
17 | }
18 |
19 | func NewConfigFromEnv() (*Config, error) {
20 | cfg := &Config{}
21 | username, err := lib.GetEnv("DB_USERNAME")
22 | if err != nil {
23 | return nil, err
24 | }
25 | cfg.user = username
26 |
27 | cfg.password = os.Getenv("DB_PASSWORD")
28 |
29 | cfg.host = os.Getenv("DB_HOST")
30 | cfg.port = os.Getenv("DB_PORT")
31 |
32 | database, err := lib.GetEnv("DB_DATABASE")
33 | if err != nil {
34 | return nil, err
35 | }
36 | cfg.DBName = database
37 |
38 | cfg.searchPath = os.Getenv("DB_SEARCH_PATH")
39 |
40 | cfg.paramText = os.Getenv("DB_PARAM_TEXT")
41 |
42 | return cfg, nil
43 | }
44 |
45 | // GetKVConnectionString returns libpq's connection string
46 | // c.f) https://pkg.go.dev/github.com/lib/pq#hdr-Connection_String_Parameters
47 | func (cfg *Config) GetKVConnectionString() string {
48 | var texts []string
49 | if cfg.user != "" {
50 | texts = append(texts, "user='"+cfg.user+"'")
51 | }
52 | if cfg.password != "" {
53 | texts = append(texts, "password='"+cfg.password+"'")
54 | }
55 | if cfg.host != "" {
56 | texts = append(texts, "host='"+cfg.host+"'")
57 | }
58 | if cfg.port != "" {
59 | texts = append(texts, "port='"+cfg.port+"'")
60 | }
61 | if cfg.DBName != "" {
62 | texts = append(texts, "dbname='"+cfg.DBName+"'")
63 | }
64 | if cfg.paramText != "" {
65 | texts = append(texts, cfg.paramText)
66 | }
67 | texts = append(texts, "sslmode=disable")
68 |
69 | return strings.Join(texts, " ")
70 | }
71 |
72 | func (cfg *Config) GetSearchPathOrPublic() string {
73 | if cfg.searchPath == "" {
74 | return "public"
75 | }
76 |
77 | return cfg.searchPath
78 | }
79 |
--------------------------------------------------------------------------------
/infra/postgres/db.go:
--------------------------------------------------------------------------------
1 | package postgres
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "github.com/jmoiron/sqlx"
7 | _ "github.com/lib/pq"
8 | "github.com/pkg/errors"
9 | )
10 |
11 | type DB struct {
12 | db *sqlx.DB
13 | }
14 |
15 | func OpenPostgresDB(cfg *Config) (*DB, error) {
16 | db, err := sqlx.Open("postgres", cfg.GetKVConnectionString())
17 | if err != nil {
18 | return nil, errors.Wrap(err, "failed to connect")
19 | }
20 |
21 | return NewDB(db), nil
22 | }
23 |
24 | func NewDB(db *sqlx.DB) *DB {
25 | return &DB{db: db}
26 | }
27 |
28 | func (db *DB) Close() error {
29 | return db.db.Close()
30 | }
31 |
32 | func (db *DB) Exec(query string) (sql.Result, error) {
33 | res, err := db.db.Exec(query)
34 | if err != nil {
35 | return nil, errors.Wrap(err, fmt.Sprintf("failed to execute query: %s", query))
36 | }
37 |
38 | return res, nil
39 | }
40 |
41 | func (db *DB) ExplainWithAnalyze(query string) ([]string, error) {
42 | var res []string
43 | err := db.db.Select(&res, "EXPLAIN (ANALYZE, BUFFERS) "+query)
44 | if err != nil {
45 | return nil, errors.Wrap(err, "failed to select")
46 | }
47 |
48 | return res, nil
49 | }
50 |
51 | func (db *DB) GetTableColumns(schema string, tables []string) ([]*ColumnInfo, error) {
52 | query, args, err := sqlx.In(columnFetchQuery, schema, tables)
53 | if err != nil {
54 | return nil, errors.Wrap(err, "failed to build query to get table schema")
55 | }
56 | query = db.db.Rebind(query)
57 |
58 | var cols []*ColumnInfo
59 | err = db.db.Select(&cols, query, args...)
60 | if err != nil {
61 | return nil, errors.Wrap(err, "failed to execute query to get table column")
62 | }
63 |
64 | return cols, nil
65 | }
66 |
67 | func (db *DB) GetIndexes(database string, tables []string) ([]*IndexInfo, error) {
68 | query, args, err := sqlx.In(indexFetchQuery, database, tables)
69 | if err != nil {
70 | return nil, errors.Wrap(err, "failed to build query to get table schema")
71 | }
72 | query = db.db.Rebind(query)
73 |
74 | var qInfos []*queryIndexInfo
75 | err = db.db.Select(&qInfos, query, args...)
76 | if err != nil {
77 | return nil, errors.Wrap(err, "failed to execute query to get indexes")
78 | }
79 |
80 | return toIndexInfo(qInfos), nil
81 | }
82 |
--------------------------------------------------------------------------------
/infra/postgres/index_info.go:
--------------------------------------------------------------------------------
1 | package postgres
2 |
3 | import (
4 | "github.com/lib/pq"
5 | "github.com/samber/lo"
6 | )
7 |
8 | const indexFetchQuery = `
9 | SELECT
10 | i.relname AS index_name,
11 | t.relname AS table_name,
12 | ARRAY(
13 | SELECT PG_GET_INDEXDEF(pg_index.indexrelid, k + 1, TRUE)
14 | FROM GENERATE_SUBSCRIPTS(pg_index.indkey, 1) AS k
15 | ORDER BY k
16 | ) AS column_names
17 | FROM pg_index
18 | INNER JOIN pg_class AS i ON pg_index.indexrelid = i.oid
19 | INNER JOIN pg_class AS t ON pg_index.indrelid = t.oid
20 | INNER JOIN pg_namespace ON i.relnamespace = pg_namespace.oid
21 | WHERE
22 | pg_namespace.nspname = ? AND
23 | t.relname IN (?)
24 | ORDER BY
25 | t.relname,
26 | i.relname
27 | `
28 |
29 | type IndexInfo struct {
30 | TableName string
31 | Columns []string
32 | }
33 |
34 | type queryIndexInfo struct {
35 | IndexName string `db:"index_name"`
36 | TableName string `db:"table_name"`
37 | ColumnNames pq.StringArray `db:"column_names"`
38 | }
39 |
40 | func toIndexInfo(queryInfos []*queryIndexInfo) []*IndexInfo {
41 | return lo.Map(queryInfos, func(qi *queryIndexInfo, _ int) *IndexInfo {
42 | return &IndexInfo{
43 | TableName: qi.TableName,
44 | Columns: qi.ColumnNames,
45 | }
46 | })
47 | }
48 |
--------------------------------------------------------------------------------
/infra/rdb/db.go:
--------------------------------------------------------------------------------
1 | package rdb
2 |
3 | import "database/sql"
4 |
5 | type DB interface {
6 | Exec(query string) (sql.Result, error)
7 | }
8 |
--------------------------------------------------------------------------------
/injection/bin_info.go:
--------------------------------------------------------------------------------
1 | package injection
2 |
3 | var BinInfo = binInfo{}
4 |
5 | type binInfo struct {
6 | Version string
7 | Commit string
8 | }
9 |
10 | func SetBinInfo(version, commit string) {
11 | BinInfo.Version = version
12 | BinInfo.Commit = commit
13 | }
14 |
--------------------------------------------------------------------------------
/injection/client_dist.go:
--------------------------------------------------------------------------------
1 | package injection
2 |
3 | import (
4 | "io/fs"
5 | )
6 |
7 | var ClientDist fs.FS
8 |
--------------------------------------------------------------------------------
/lab/micro/.gitignore:
--------------------------------------------------------------------------------
1 | bff/node_modules
2 |
3 | docker/*/data
4 |
--------------------------------------------------------------------------------
/lab/micro/bff/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18
2 |
3 | WORKDIR /app
4 |
5 | CMD ["yarn", "nodemon", "server.js"]
6 |
--------------------------------------------------------------------------------
/lab/micro/bff/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@opentelemetry/api": "^1.2.0",
4 | "@opentelemetry/core": "^1.7.0",
5 | "@opentelemetry/exporter-jaeger": "^1.7.0",
6 | "@opentelemetry/instrumentation": "^0.33.0",
7 | "@opentelemetry/instrumentation-express": "^0.31.3",
8 | "@opentelemetry/instrumentation-graphql": "^0.32.0",
9 | "@opentelemetry/instrumentation-http": "^0.33.0",
10 | "@opentelemetry/resources": "^1.7.0",
11 | "@opentelemetry/sdk-trace-base": "^1.7.0",
12 | "@opentelemetry/sdk-trace-node": "^1.7.0",
13 | "express": "^4.18.2",
14 | "express-graphql": "^0.12.0",
15 | "graphql": "^16.6.0",
16 | "nodemon": "^2.0.20"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lab/micro/bff/server.js:
--------------------------------------------------------------------------------
1 | require("./tel.js");
2 | const express = require("express");
3 | const { graphqlHTTP } = require("express-graphql");
4 | const { buildSchema } = require("graphql");
5 | const opentelemetry = require("@opentelemetry/api");
6 |
7 | const schema = buildSchema(`
8 | type Query {
9 | users(ids: [Int!]!): [User!]!
10 | }
11 |
12 | type User {
13 | id: Int!
14 | name: String!
15 | email: String!
16 | histories: [PaymentHistory!]
17 | }
18 |
19 | type PaymentHistory {
20 | amount: Int!
21 | last_numbers: String!
22 | }
23 | `);
24 |
25 | const getTraceId = () => {
26 | const span = opentelemetry.trace.getActiveSpan();
27 | const ctx = span.spanContext();
28 |
29 | return `00-${ctx.traceId}-${ctx.spanId}-00`;
30 | };
31 |
32 | const handleUsers = async (args) => {
33 | const ids = args.ids;
34 | const traceId = getTraceId();
35 | const promises = ids.map((id) =>
36 | (async function (id) {
37 | return fetch(`http://user:9002/users/${id}`, {
38 | headers: {
39 | // Couldn't find a way to inject traceparent header automatically
40 | traceparent: traceId,
41 | },
42 | })
43 | .then((r) => r.json())
44 | .then((v) => ({
45 | id: v.id,
46 | name: v.name,
47 | email: v.email,
48 | histories: v.histories.map((h) => ({
49 | amount: h.amount,
50 | last_numbers: h.last_numbers,
51 | })),
52 | }));
53 | })(id)
54 | );
55 | return await Promise.all(promises);
56 | };
57 |
58 | const root = {
59 | users: handleUsers,
60 | };
61 |
62 | const app = express();
63 | app.use(
64 | "/graphql",
65 | graphqlHTTP({
66 | schema: schema,
67 | rootValue: root,
68 | })
69 | );
70 | app.listen(9001);
71 | console.log("Running a BFF service at http://localhost:9001/graphql");
72 |
--------------------------------------------------------------------------------
/lab/micro/bff/tel.js:
--------------------------------------------------------------------------------
1 | const { Resource } = require('@opentelemetry/resources');
2 | const { HttpInstrumentation } = require ('@opentelemetry/instrumentation-http');
3 | const { SimpleSpanProcessor } = require ("@opentelemetry/sdk-trace-base");
4 | const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
5 | const { registerInstrumentations } = require('@opentelemetry/instrumentation');
6 | const { ExpressInstrumentation } = require ('@opentelemetry/instrumentation-express');
7 | const { GraphQLInstrumentation } = require("@opentelemetry/instrumentation-graphql");
8 | const { JaegerExporter } = require("@opentelemetry/exporter-jaeger");
9 | const { W3CTraceContextPropagator } = require("@opentelemetry/core");
10 | const opentelemetry = require("@opentelemetry/api");
11 |
12 | registerInstrumentations({
13 | instrumentations: [
14 | new HttpInstrumentation(),
15 | new ExpressInstrumentation(),
16 | new GraphQLInstrumentation()
17 | ]
18 | })
19 |
20 | const provider = new NodeTracerProvider({
21 | resource: Resource.default().merge(new Resource({
22 | "service.name": "bff"
23 | }))
24 | })
25 |
26 | const jaegerExporter = new JaegerExporter({
27 | endpoint: "http://jaeger:14268/api/traces"
28 | })
29 | provider.addSpanProcessor(
30 | new SimpleSpanProcessor(jaegerExporter)
31 | );
32 |
33 | opentelemetry.propagation.setGlobalPropagator( new W3CTraceContextPropagator())
34 | provider.register()
35 |
--------------------------------------------------------------------------------
/lab/micro/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | bff:
5 | build: ./bff
6 | volumes:
7 | - ./bff:/app
8 | ports:
9 | - "9001:9001"
10 | user:
11 | build: ./user
12 | volumes:
13 | - ./user:/app
14 | ports:
15 | - "9002:9002"
16 | restart: unless-stopped
17 | user_db_mysql:
18 | image: mysql:8.0.23
19 | volumes:
20 | - ./docker/user_db_mysql/data:/var/lib/mysql
21 | - ./docker/user_db_mysql/init:/docker-entrypoint-initdb.d
22 | ports:
23 | - "9006:3306"
24 | environment:
25 | MYSQL_DATABASE: user_db
26 | MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
27 | jaeger:
28 | image: jaegertracing/all-in-one:1.38
29 | ports:
30 | - "6831:6831/udp"
31 | - "6832:6832/udp"
32 | - "5778:5778"
33 | - "4317:4317"
34 | - "4318:4318"
35 | - "14250:14250"
36 | - "14268:14268"
37 | - "14269:14269"
38 | - "9411:9411"
39 | - "16685:16685"
40 | - "16686:16686"
41 | - "16687:16687"
42 | environment:
43 | COLLECTOR_ZIPKIN_HOST_PORT: ":9411"
44 | COLLECTOR_OTLP_ENABLED: true
45 |
--------------------------------------------------------------------------------
/lab/micro/docker/user_db_mysql/init/script.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE users (
2 | id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
3 | name varchar(255) NOT NULL,
4 | email varchar(255) NOT NULL,
5 | password varchar(255) NOT NULL
6 | );
7 |
8 | CREATE TABLE payment_methods (
9 | id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
10 | user_id int NOT NULL,
11 | last_numbers varchar(10) NOT NULL
12 | );
13 |
14 | CREATE TABLE payment_histories (
15 | id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
16 | user_id int NOT NULL,
17 | payment_method_id int NOT NULL,
18 | amount int NOT NULL,
19 | payed_at datetime NOT NULL
20 | );
21 |
22 | /* Insert 16 rows */
23 | INSERT INTO users(id, name, email, password)
24 | WITH l0 AS (SELECT 0 AS num UNION ALL SELECT 1),
25 | l1 AS (SELECT a.num * 2 + b.num AS num FROM l0 AS a CROSS JOIN l0 AS b),
26 | l2 AS (SELECT a.num * 4 + b.num AS num FROM l1 AS a CROSS JOIN l1 AS b)
27 | SELECT num+1,
28 | CONCAT('test', CAST(num AS char)),
29 | CONCAT('test', CAST(num AS char), '@example.com'),
30 | 'p@ssw0rd'
31 | FROM l2;
32 |
33 | INSERT INTO payment_methods(id, user_id, last_numbers)
34 | SELECT
35 | id,
36 | id,
37 | id%10*1111
38 | FROM users;
39 |
40 | /* Insert 16*16 rows */
41 | INSERT INTO payment_histories(id, user_id, payment_method_id, amount, payed_at)
42 | WITH l0 AS (SELECT 0 AS num UNION ALL SELECT 1),
43 | l1 AS (SELECT a.num * 2 + b.num AS num FROM l0 AS a CROSS JOIN l0 AS b),
44 | l2 AS (SELECT a.num * 4 + b.num AS num FROM l1 AS a CROSS JOIN l1 AS b)
45 | SELECT us.id*16+num,
46 | us.id,
47 | us.id,
48 | (us.id*16+num) * 100,
49 | NOW() - INTERVAL (num%16) MONTH
50 | FROM l2 CROSS JOIN (SELECT id FROM users) AS us;
51 |
--------------------------------------------------------------------------------
/lab/micro/user/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.19
2 |
3 | WORKDIR /app
4 |
5 | COPY go.mod .
6 | COPY go.sum .
7 |
8 | RUN go mod download
9 |
10 | CMD ["go", "run", "."]
11 |
--------------------------------------------------------------------------------
/lab/micro/user/db.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "gorm.io/driver/mysql"
6 | "gorm.io/gorm"
7 | "gorm.io/plugin/opentelemetry/tracing"
8 | "time"
9 | )
10 |
11 | var globalDB *gorm.DB
12 |
13 | func initDB(dsn string) error {
14 | var err error
15 | globalDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
16 | if err != nil {
17 | return err
18 | }
19 | if err := globalDB.Use(tracing.NewPlugin(tracing.WithoutMetrics())); err != nil {
20 | return err
21 | }
22 |
23 | return nil
24 | }
25 |
26 | func dbWithCtx(ctx context.Context) *gorm.DB {
27 | return globalDB.WithContext(ctx)
28 | }
29 |
30 | type User struct {
31 | ID int32 `json:"id"`
32 | Name string `json:"name"`
33 | Email string `json:"email"`
34 | }
35 |
36 | type PaymentHistory struct {
37 | ID int32
38 | UserID int32
39 | PaymentMethodID int32
40 | Amount int32
41 | PayedAt time.Time
42 | }
43 |
44 | type PaymentMethod struct {
45 | ID int32
46 | UserID int32
47 | LastNumbers string
48 | }
49 |
--------------------------------------------------------------------------------
/lab/micro/user/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mrasu/GravityR/jaeger/user
2 |
3 | go 1.19
4 |
5 | require (
6 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.4
7 | go.opentelemetry.io/otel v1.11.1
8 | go.opentelemetry.io/otel/exporters/jaeger v1.11.1
9 | go.opentelemetry.io/otel/sdk v1.11.1
10 | go.opentelemetry.io/otel/trace v1.11.1
11 | gorm.io/driver/mysql v1.4.3
12 | gorm.io/gorm v1.24.0
13 | gorm.io/plugin/opentelemetry v0.1.0
14 | )
15 |
16 | require (
17 | github.com/felixge/httpsnoop v1.0.3 // indirect
18 | github.com/go-logr/logr v1.2.3 // indirect
19 | github.com/go-logr/stdr v1.2.2 // indirect
20 | github.com/go-sql-driver/mysql v1.6.0 // indirect
21 | github.com/jinzhu/inflection v1.0.0 // indirect
22 | github.com/jinzhu/now v1.1.5 // indirect
23 | go.opentelemetry.io/otel/metric v0.33.0 // indirect
24 | golang.org/x/sys v0.1.0 // indirect
25 | )
26 |
--------------------------------------------------------------------------------
/lab/micro/user/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
7 | "go.opentelemetry.io/otel/attribute"
8 | "go.opentelemetry.io/otel/trace"
9 | "net/http"
10 | "path/filepath"
11 | "strconv"
12 | "strings"
13 | )
14 |
15 | const (
16 | dbDSN = "root:@tcp(user_db_mysql:3306)/user_db?charset=utf8mb4&parseTime=True&loc=Local"
17 | )
18 |
19 | func main() {
20 | err := initDB(dbDSN)
21 | if err != nil {
22 | panic(err)
23 | }
24 |
25 | err = initTraceProvider("http://jaeger:14268/api/traces")
26 | if err != nil {
27 | panic(err)
28 | }
29 |
30 | otelHandler := otelhttp.NewHandler(http.HandlerFunc(usersHandler), "users")
31 | http.Handle("/users/", otelHandler)
32 | fmt.Println("Running a user service at http://localhost:9002")
33 | http.ListenAndServe(":9002", nil)
34 | }
35 |
36 | func usersHandler(w http.ResponseWriter, r *http.Request) {
37 | db := dbWithCtx(r.Context())
38 | fmt.Println("=========")
39 | fmt.Println(r.Header)
40 | fmt.Println(trace.SpanContextFromContext(r.Context()).TraceID().String())
41 |
42 | sub := strings.TrimPrefix(r.URL.Path, "/users")
43 | _, file := filepath.Split(sub)
44 | id, err := strconv.Atoi(file)
45 | if err != nil {
46 | panic(err)
47 | }
48 | span := trace.SpanFromContext(r.Context())
49 | span.SetAttributes(attribute.Key("user_id").Int(id))
50 | fmt.Printf("getting user data for id=%d", id)
51 |
52 | var u *User
53 | if err := db.Where("id = ?", id).First(&u).Error; err != nil {
54 | panic(err)
55 | }
56 | var histories []*PaymentHistory
57 | if err := db.Where("user_id = ?", u.ID).Find(&histories).Error; err != nil {
58 | panic(err)
59 | }
60 |
61 | var resHistories []map[string]any
62 | for _, h := range histories {
63 | var m *PaymentMethod
64 | if err := db.Where("id = ?", h.PaymentMethodID).Find(&m).Error; err != nil {
65 | panic(err)
66 | }
67 | resHistories = append(resHistories, map[string]any{
68 | "amount": h.Amount,
69 | "last_numbers": m.LastNumbers,
70 | })
71 | }
72 |
73 | res := map[string]any{
74 | "id": u.ID,
75 | "name": u.Name,
76 | "email": u.Email,
77 | "histories": resHistories,
78 | }
79 | out, err := json.Marshal(res)
80 | if err != nil {
81 | panic(err)
82 | }
83 |
84 | w.Write(out)
85 | }
86 |
--------------------------------------------------------------------------------
/lab/micro/user/otel.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "go.opentelemetry.io/otel"
5 | "go.opentelemetry.io/otel/exporters/jaeger"
6 | "go.opentelemetry.io/otel/propagation"
7 | "go.opentelemetry.io/otel/sdk/resource"
8 | sdktrace "go.opentelemetry.io/otel/sdk/trace"
9 | semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
10 | )
11 |
12 | const (
13 | serviceName = "user"
14 | )
15 |
16 | func initTraceProvider(url string) error {
17 | exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
18 | if err != nil {
19 | return err
20 | }
21 | tp := sdktrace.NewTracerProvider(
22 | sdktrace.WithSampler(sdktrace.AlwaysSample()),
23 | sdktrace.WithBatcher(exp),
24 | sdktrace.WithResource(resource.NewWithAttributes(
25 | semconv.SchemaURL,
26 | semconv.ServiceNameKey.String(serviceName),
27 | )),
28 | )
29 | otel.SetTracerProvider(tp)
30 | otel.SetTextMapPropagator(propagation.TraceContext{})
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/lib/env.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | "os"
6 | )
7 |
8 | func GetEnv(key string) (string, error) {
9 | v := os.Getenv(key)
10 | if len(v) == 0 {
11 | return "", errors.Errorf("env: %s is not set", key)
12 | }
13 | return v, nil
14 | }
15 |
--------------------------------------------------------------------------------
/lib/env_test.go:
--------------------------------------------------------------------------------
1 | package lib_test
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "github.com/stretchr/testify/assert"
6 | "github.com/stretchr/testify/require"
7 | "os"
8 | "testing"
9 | )
10 |
11 | func TestGetEnv(t *testing.T) {
12 | key := "test_dummy"
13 | os.Setenv(key, "dummy")
14 |
15 | v, err := lib.GetEnv(key)
16 | require.NoError(t, err)
17 | assert.Equal(t, "dummy", v)
18 | }
19 |
20 | func TestGetEnv_error(t *testing.T) {
21 | v, err := lib.GetEnv("invalid_key")
22 | assert.ErrorContains(t, err, "not set")
23 | assert.Equal(t, v, "")
24 | }
25 |
--------------------------------------------------------------------------------
/lib/errors.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import "github.com/pkg/errors"
4 |
5 | type UnsupportedError struct {
6 | message string
7 | }
8 |
9 | func NewUnsupportedError(msg string) error {
10 | return errors.Wrap(&UnsupportedError{msg}, "")
11 | }
12 |
13 | func (ue *UnsupportedError) Error() string {
14 | return ue.message
15 | }
16 |
17 | type InvalidAstError struct {
18 | message string
19 | }
20 |
21 | func NewInvalidAstError(msg string) error {
22 | return errors.Wrap(&InvalidAstError{msg}, "")
23 | }
24 |
25 | func (ie *InvalidAstError) Error() string {
26 | return ie.message
27 | }
28 |
--------------------------------------------------------------------------------
/lib/heap.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "container/heap"
5 | )
6 |
7 | var _ heap.Interface = &anyHeap[any]{}
8 |
9 | type LimitedHeap[T any] struct {
10 | h *anyHeap[T]
11 | limit int
12 | }
13 |
14 | func NewLimitedHeap[T any](limit int, less func(a, b T) bool) *LimitedHeap[T] {
15 | h := &anyHeap[T]{less: less}
16 | heap.Init(h)
17 | return &LimitedHeap[T]{
18 | h: h,
19 | limit: limit,
20 | }
21 | }
22 |
23 | func (h LimitedHeap[T]) Len() int {
24 | return h.h.Len()
25 | }
26 |
27 | func (h LimitedHeap[T]) Push(x T) {
28 | heap.Push(h.h, x)
29 | if h.h.Len() > h.limit {
30 | heap.Pop(h.h)
31 | }
32 | }
33 |
34 | func (h LimitedHeap[T]) PopAll() []T {
35 | var res []T
36 | for h.h.Len() > 0 {
37 | res = append(res, heap.Pop(h.h).(T))
38 | }
39 |
40 | return res
41 | }
42 |
43 | type anyHeap[T any] struct {
44 | vals []T
45 | less func(a, b T) bool
46 | }
47 |
48 | func (h *anyHeap[T]) Len() int {
49 | return len(h.vals)
50 | }
51 |
52 | func (h *anyHeap[T]) Less(i, j int) bool {
53 | return h.less(h.vals[i], h.vals[j])
54 | }
55 |
56 | func (h *anyHeap[T]) Swap(i, j int) {
57 | h.vals[i], h.vals[j] = h.vals[j], h.vals[i]
58 | }
59 |
60 | func (h *anyHeap[T]) Push(x any) {
61 | v := x.(T)
62 | h.vals = append(h.vals, v)
63 | }
64 |
65 | func (h *anyHeap[T]) Pop() any {
66 | l := len(h.vals) - 1
67 | v := h.vals[l]
68 | h.vals = h.vals[0:l]
69 |
70 | return v
71 | }
72 |
--------------------------------------------------------------------------------
/lib/heap_test.go:
--------------------------------------------------------------------------------
1 | package lib_test
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "github.com/stretchr/testify/assert"
6 | "testing"
7 | )
8 |
9 | func TestLimitedHeap_Push(t *testing.T) {
10 | less := func(v1, v2 int) bool {
11 | return v1 < v2
12 | }
13 | tests := []struct {
14 | name string
15 | vals []int
16 | want []int
17 | }{
18 | {
19 | name: "Keep when not reach the limit",
20 | vals: []int{1, 5, 3, 2, 4},
21 | want: []int{1, 2, 3, 4, 5},
22 | },
23 | {
24 | name: "Omit the smallest element when exceeds the limit",
25 | vals: []int{1, 5, 3, 2, 4, 6},
26 | want: []int{2, 3, 4, 5, 6},
27 | },
28 | }
29 | for _, tt := range tests {
30 | t.Run(tt.name, func(t *testing.T) {
31 | h := lib.NewLimitedHeap(5, less)
32 | for _, v := range tt.vals {
33 | h.Push(v)
34 | }
35 | assert.Equal(t, tt.want, h.PopAll())
36 | })
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/http_transport.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "github.com/rs/zerolog/log"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | type HttpTransport struct {
10 | Transport http.RoundTripper
11 | }
12 |
13 | func NewHttpTransport() *HttpTransport {
14 | return &HttpTransport{Transport: http.DefaultTransport}
15 | }
16 |
17 | func (ht *HttpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
18 | log.Printf("[http] --> %s %s", req.Method, req.URL)
19 | startTime := time.Now()
20 |
21 | resp, err := ht.Transport.RoundTrip(req)
22 |
23 | duration := time.Since(startTime)
24 | duration /= time.Millisecond
25 |
26 | if err != nil {
27 | log.Printf("[http] <-- ERROR method=%s host=%s path=%s status=error durationMs=%d error=%q", req.Method, req.Host, req.URL.Path, duration, err.Error())
28 | return nil, err
29 | }
30 |
31 | log.Printf("[http] <-- method=%s host=%s path=%s status=%d durationMs=%d", req.Method, req.Host, req.URL.Path, resp.StatusCode, duration)
32 | return resp, nil
33 | }
34 |
--------------------------------------------------------------------------------
/lib/join.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import "strings"
4 |
5 | func Join[T any](vals []T, sep string, f func(T) string) string {
6 | var res []string
7 | for _, val := range vals {
8 | res = append(res, f(val))
9 | }
10 | return strings.Join(res, sep)
11 | }
12 |
--------------------------------------------------------------------------------
/lib/join_test.go:
--------------------------------------------------------------------------------
1 | package lib_test
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "github.com/stretchr/testify/assert"
6 | "strconv"
7 | "testing"
8 | )
9 |
10 | func TestJoin(t *testing.T) {
11 | tests := []struct {
12 | name string
13 | vals []int
14 | sep string
15 | expected string
16 | }{
17 | {
18 | name: "Concat text from the result of function",
19 | vals: []int{1, 2, 3},
20 | sep: "*",
21 | expected: "1*2*3",
22 | },
23 | {
24 | name: "Empty when arg is empty",
25 | vals: []int{},
26 | sep: "*",
27 | expected: "",
28 | },
29 | }
30 | for _, tt := range tests {
31 | t.Run(tt.name, func(t *testing.T) {
32 | res := lib.Join(tt.vals, tt.sep, func(i int) string { return strconv.Itoa(i) })
33 | assert.Equal(t, tt.expected, res)
34 | })
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/ptr.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | func Ptr[T any](v T) *T {
4 | return &v
5 | }
6 |
--------------------------------------------------------------------------------
/lib/set.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | type Set[T comparable] struct {
4 | vals map[T]bool
5 | }
6 |
7 | func NewSet[T comparable]() *Set[T] {
8 | return &Set[T]{map[T]bool{}}
9 | }
10 |
11 | func NewSetS[T comparable](origins []T) *Set[T] {
12 | vals := map[T]bool{}
13 | for _, o := range origins {
14 | vals[o] = true
15 | }
16 | return &Set[T]{vals}
17 | }
18 |
19 | func (s *Set[T]) Add(val T) {
20 | s.vals[val] = true
21 | }
22 |
23 | func (s *Set[T]) Merge(os *Set[T]) {
24 | for _, val := range os.Values() {
25 | s.Add(val)
26 | }
27 | }
28 |
29 | func (s *Set[T]) Delete(val T) {
30 | delete(s.vals, val)
31 | }
32 |
33 | func (s *Set[T]) Values() []T {
34 | var vals []T
35 | for val := range s.vals {
36 | vals = append(vals, val)
37 | }
38 |
39 | return vals
40 | }
41 |
42 | func (s *Set[T]) Contains(val T) bool {
43 | if _, ok := s.vals[val]; ok {
44 | return true
45 | }
46 | return false
47 | }
48 |
49 | func (s *Set[T]) Count() int {
50 | return len(s.vals)
51 | }
52 |
--------------------------------------------------------------------------------
/lib/slice.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | func SliceOrEmpty[T any](vals []T) []T {
4 | if vals == nil {
5 | return []T{}
6 | }
7 | return vals
8 | }
9 |
--------------------------------------------------------------------------------
/lib/slice_test.go:
--------------------------------------------------------------------------------
1 | package lib_test
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "github.com/stretchr/testify/assert"
6 | "testing"
7 | )
8 |
9 | func TestSliceOrEmpty(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | vals []int
13 | expected []int
14 | }{
15 | {
16 | name: "Return as is when not empty",
17 | vals: []int{1, 2, 3},
18 | expected: []int{1, 2, 3},
19 | },
20 | {
21 | name: "Return empty slice when empty",
22 | vals: []int{},
23 | expected: []int{},
24 | },
25 | {
26 | name: "Return empty slice when nil",
27 | vals: nil,
28 | expected: []int{},
29 | },
30 | }
31 |
32 | for _, tt := range tests {
33 | t.Run(tt.name, func(t *testing.T) {
34 | assert.Equal(t, tt.expected, lib.SliceOrEmpty(tt.vals))
35 | })
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/sort.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "golang.org/x/exp/constraints"
5 | "sort"
6 | )
7 |
8 | func Sort[T any, U constraints.Ordered](vals []T, f func(v T) U) {
9 | sort.Slice(vals, func(i, j int) bool {
10 | vi := f(vals[i])
11 | vj := f(vals[j])
12 |
13 | return vi < vj
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/lib/sort_test.go:
--------------------------------------------------------------------------------
1 | package lib_test
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "github.com/stretchr/testify/assert"
6 | "testing"
7 | )
8 |
9 | func TestSort(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | vals []int
13 | expected []int
14 | }{
15 | {
16 | name: "Sort values",
17 | vals: []int{3, 1, 2},
18 | expected: []int{1, 2, 3},
19 | },
20 | {
21 | name: "As is when empty",
22 | vals: []int{},
23 | expected: []int{},
24 | },
25 | }
26 |
27 | for _, tt := range tests {
28 | t.Run(tt.name, func(t *testing.T) {
29 | lib.Sort(tt.vals, func(v int) int { return v })
30 | assert.Equal(t, tt.expected, tt.vals)
31 | })
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/stack.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | type Stack[T any] struct {
4 | vals []*T
5 | }
6 |
7 | func NewStack[T any](vals ...*T) *Stack[T] {
8 | return &Stack[T]{vals: vals}
9 | }
10 |
11 | func (s *Stack[T]) Push(val *T) {
12 | s.vals = append(s.vals, val)
13 | }
14 |
15 | func (s *Stack[T]) Pop() *T {
16 | if len(s.vals) == 0 {
17 | return nil
18 | }
19 |
20 | v := s.vals[len(s.vals)-1]
21 | s.vals = s.vals[:len(s.vals)-1]
22 |
23 | return v
24 | }
25 |
26 | func (s *Stack[T]) Top() *T {
27 | if len(s.vals) == 0 {
28 | return nil
29 | }
30 |
31 | return s.vals[len(s.vals)-1]
32 | }
33 |
34 | func (s *Stack[T]) Size() int {
35 | return len(s.vals)
36 | }
37 |
--------------------------------------------------------------------------------
/lib/stack_test.go:
--------------------------------------------------------------------------------
1 | package lib_test
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "github.com/stretchr/testify/assert"
6 | "testing"
7 | )
8 |
9 | type data struct {
10 | val int
11 | }
12 |
13 | func TestStack_Push(t *testing.T) {
14 | tests := []struct {
15 | name string
16 | vals []*data
17 | expectedValues []*data
18 | }{
19 | {
20 | name: "Push once",
21 | vals: []*data{{1}},
22 | expectedValues: []*data{{1}},
23 | },
24 | {
25 | name: "Push twice",
26 | vals: []*data{{1}, {2}},
27 | expectedValues: []*data{{2}, {1}},
28 | },
29 | }
30 |
31 | for _, tt := range tests {
32 | t.Run(tt.name, func(t *testing.T) {
33 | s := lib.NewStack[data]()
34 | for _, v := range tt.vals {
35 | s.Push(v)
36 | }
37 | actualValues := popAllValues(s)
38 | assert.Equal(t, tt.expectedValues, actualValues)
39 | })
40 | }
41 | }
42 |
43 | func TestStack_Top(t *testing.T) {
44 | tests := []struct {
45 | name string
46 | vals []*data
47 | expected *data
48 | }{
49 | {
50 | name: "Return the last element",
51 | vals: []*data{{1}, {2}, {3}},
52 | expected: &data{3},
53 | },
54 | {
55 | name: "Return nil when empty",
56 | vals: []*data{},
57 | expected: nil,
58 | },
59 | }
60 |
61 | for _, tt := range tests {
62 | t.Run(tt.name, func(t *testing.T) {
63 | s := lib.NewStack[data]()
64 | for _, v := range tt.vals {
65 | s.Push(v)
66 | }
67 |
68 | assert.Equal(t, tt.expected, s.Top())
69 | })
70 | }
71 | }
72 |
73 | func popAllValues[T any](s *lib.Stack[T]) []*T {
74 | var res []*T
75 | for {
76 | v := s.Pop()
77 | if v == nil {
78 | break
79 | }
80 | res = append(res, v)
81 | }
82 |
83 | return res
84 | }
85 |
--------------------------------------------------------------------------------
/lib/time.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import "time"
4 |
5 | func NormalizeTimeByHour(t time.Time) time.Time {
6 | return time.Date(
7 | t.Year(), t.Month(), t.Day(),
8 | t.Hour(), 0, 0, 0,
9 | t.Location(),
10 | )
11 | }
12 |
13 | func GenerateTimeRanges(start, end time.Time, interval time.Duration) []time.Time {
14 | var res []time.Time
15 | for t := start; t.Before(end); t = t.Add(interval) {
16 | res = append(res, t)
17 | }
18 |
19 | return res
20 | }
21 |
--------------------------------------------------------------------------------
/lib/time_test.go:
--------------------------------------------------------------------------------
1 | package lib_test
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "github.com/stretchr/testify/assert"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func TestNormalizeTimeByHour(t *testing.T) {
11 | tests := []struct {
12 | name string
13 | t time.Time
14 | expected time.Time
15 | }{
16 | {
17 | name: "Make zeros under hour",
18 | t: time.Date(2001, 2, 3, 4, 5, 6, 7, time.UTC),
19 | expected: time.Date(2001, 2, 3, 4, 0, 0, 0, time.UTC),
20 | },
21 | }
22 |
23 | for _, tt := range tests {
24 | t.Run(tt.name, func(t *testing.T) {
25 | assert.Equal(t, tt.expected, lib.NormalizeTimeByHour(tt.t))
26 | })
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/uniq.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | func UniqBy[T any, U comparable](vals []T, fn func(T) U) []T {
4 | var res []T
5 | found := NewSet[U]()
6 |
7 | for _, v := range vals {
8 | u := fn(v)
9 | if found.Contains(u) {
10 | continue
11 | }
12 | res = append(res, v)
13 | found.Add(u)
14 | }
15 |
16 | return res
17 | }
18 |
--------------------------------------------------------------------------------
/lib/uniq_test.go:
--------------------------------------------------------------------------------
1 | package lib_test
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "github.com/stretchr/testify/assert"
6 | "testing"
7 | )
8 |
9 | func TestUniqBy(t *testing.T) {
10 | type data struct{ val int }
11 |
12 | tests := []struct {
13 | name string
14 | vals []data
15 | fn func(data) int
16 | expected []data
17 | }{
18 | {
19 | name: "Remove duplications",
20 | vals: []data{{1}, {2}, {3}, {2}, {1}},
21 | fn: func(d data) int { return d.val },
22 | expected: []data{{1}, {2}, {3}},
23 | },
24 | }
25 |
26 | for _, tt := range tests {
27 | t.Run(tt.name, func(t *testing.T) {
28 | res := lib.UniqBy(tt.vals, tt.fn)
29 | assert.ElementsMatch(t, tt.expected, res)
30 | })
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "github.com/mrasu/GravityR/cmd"
6 | "github.com/mrasu/GravityR/injection"
7 | "github.com/mrasu/GravityR/lib"
8 | "net/http"
9 | )
10 |
11 | //go:embed client/dist/assets/*
12 | var clientDist embed.FS
13 |
14 | var (
15 | version = "0.0.0"
16 | commit = "none"
17 | )
18 |
19 | func main() {
20 | injection.ClientDist = clientDist
21 | injection.SetBinInfo(version, commit)
22 |
23 | http.DefaultTransport = lib.NewHttpTransport()
24 |
25 | cmd.Execute()
26 | }
27 |
--------------------------------------------------------------------------------
/otel/jaeger/trace_fetcher.go:
--------------------------------------------------------------------------------
1 | package jaeger
2 |
3 | import (
4 | "github.com/gogo/protobuf/types"
5 | "github.com/jaegertracing/jaeger/proto-gen/api_v3"
6 | "github.com/mrasu/GravityR/infra/jaeger"
7 | "github.com/mrasu/GravityR/lib"
8 | "github.com/mrasu/GravityR/otel/omodel"
9 | "github.com/rs/zerolog/log"
10 | "time"
11 | )
12 |
13 | type TraceFetcher struct {
14 | cli *jaeger.Client
15 | }
16 |
17 | func NewTraceFetcher(cli *jaeger.Client) *TraceFetcher {
18 | return &TraceFetcher{cli: cli}
19 | }
20 |
21 | func (tf *TraceFetcher) FetchCompactedTraces(size int32, start, end time.Time, serviceName string, durationMin time.Duration) ([]*omodel.TraceTree, error) {
22 | interval := 30 * time.Minute
23 |
24 | var res []*omodel.TraceTree
25 | times := lib.GenerateTimeRanges(start, end, interval)
26 | for _, start := range times {
27 | param := &api_v3.TraceQueryParameters{
28 | ServiceName: serviceName,
29 | StartTimeMin: &types.Timestamp{Seconds: start.Unix()},
30 | StartTimeMax: &types.Timestamp{Seconds: start.Add(interval).Unix()},
31 | NumTraces: size,
32 | DurationMin: tf.toProtoDuration(durationMin),
33 | }
34 | log.Info().Msgf("Getting data for %s", start.Format(time.RFC3339))
35 |
36 | trees, err := tf.findCompactedTraceTrees(param)
37 | if err != nil {
38 | return nil, err
39 | }
40 | res = append(res, trees...)
41 | }
42 |
43 | return res, nil
44 | }
45 |
46 | func (tf *TraceFetcher) toProtoDuration(duration time.Duration) *types.Duration {
47 | secs := int64(duration.Truncate(time.Second).Seconds())
48 | nanos := int32(duration.Nanoseconds() - int64(time.Duration(secs)*time.Second))
49 |
50 | return &types.Duration{
51 | Seconds: secs,
52 | Nanos: nanos,
53 | }
54 | }
55 |
56 | func (tf *TraceFetcher) findCompactedTraceTrees(param *api_v3.TraceQueryParameters) ([]*omodel.TraceTree, error) {
57 | traces, err := tf.cli.FindTraces(param)
58 | if err != nil {
59 | return nil, err
60 | }
61 |
62 | var compactedTrees []*omodel.TraceTree
63 | for _, trace := range traces {
64 | trees := ToTraceTrees(trace.Spans)
65 | for _, tree := range trees {
66 | compacted := tree.Compact()
67 | compactedTrees = append(compactedTrees, compacted)
68 | }
69 | }
70 |
71 | return compactedTrees, nil
72 | }
73 |
--------------------------------------------------------------------------------
/otel/jaeger/trace_picker.go:
--------------------------------------------------------------------------------
1 | package jaeger
2 |
3 | import (
4 | "github.com/mrasu/GravityR/lib"
5 | "github.com/mrasu/GravityR/otel/omodel"
6 | "github.com/samber/lo"
7 | )
8 |
9 | type TracePicker struct {
10 | traces []*omodel.TraceTree
11 | }
12 |
13 | func NewTracePicker(traces []*omodel.TraceTree) *TracePicker {
14 | return &TracePicker{traces: traces}
15 | }
16 |
17 | func (tp *TracePicker) PickSameServiceAccessTraces(threshold int) []*omodel.TraceTree {
18 | return lo.Filter(tp.traces, func(tree *omodel.TraceTree, _ int) bool {
19 | return tree.MaxSameServiceAccessCount() > threshold
20 | })
21 | }
22 |
23 | func (tp *TracePicker) PickSlowTraces(num int) []*omodel.TraceTree {
24 | slowTraces := lib.NewLimitedHeap[*omodel.TraceTree](num, func(a1, a2 *omodel.TraceTree) bool {
25 | d1 := a1.Root.EndTimeUnixNano - a1.Root.StartTimeUnixNano
26 | d2 := a2.Root.EndTimeUnixNano - a2.Root.StartTimeUnixNano
27 | return d1 < d2
28 | })
29 |
30 | for _, tree := range tp.traces {
31 | slowTraces.Push(tree)
32 | }
33 |
34 | return lo.Reverse(slowTraces.PopAll())
35 | }
36 |
--------------------------------------------------------------------------------
/otel/omodel/any_value.go:
--------------------------------------------------------------------------------
1 | package omodel
2 |
3 | type AnyValue struct {
4 | Val AnyValueDatum
5 | }
6 |
7 | type AnyValueDatum interface{}
8 |
9 | type AnyValueString struct {
10 | StringValue string
11 | }
12 | type AnyValueBool struct {
13 | BoolValue bool
14 | }
15 | type AnyValueInt struct {
16 | IntValue int64
17 | }
18 | type AnyValueDouble struct {
19 | DoubleValue float64
20 | }
21 | type AnyValueArray struct {
22 | ArrayValues []AnyValueDatum
23 | }
24 | type AnyValueKV struct {
25 | KVValue map[string]AnyValueDatum
26 | }
27 | type AnyValueBytes struct {
28 | BytesValue []byte
29 | }
30 |
31 | func (m *AnyValue) GetStringValue() string {
32 | if x, ok := m.Val.(*AnyValueString); ok {
33 | return x.StringValue
34 | }
35 | return ""
36 | }
37 |
38 | func (m *AnyValue) GetBoolValue() bool {
39 | if x, ok := m.Val.(*AnyValueBool); ok {
40 | return x.BoolValue
41 | }
42 | return false
43 | }
44 |
45 | func (m *AnyValue) GetIntValue() int64 {
46 | if x, ok := m.Val.(*AnyValueInt); ok {
47 | return x.IntValue
48 | }
49 | return 0
50 | }
51 |
52 | func (m *AnyValue) GetDoubleValue() float64 {
53 | if x, ok := m.Val.(*AnyValueDouble); ok {
54 | return x.DoubleValue
55 | }
56 | return 0
57 | }
58 |
59 | func (m *AnyValue) GetArrayValue() []AnyValueDatum {
60 | if x, ok := m.Val.(*AnyValueArray); ok {
61 | return x.ArrayValues
62 | }
63 | return nil
64 | }
65 |
66 | func (m *AnyValue) GetKV() map[string]AnyValueDatum {
67 | if x, ok := m.Val.(*AnyValueKV); ok {
68 | return x.KVValue
69 | }
70 | return nil
71 | }
72 |
73 | func (m *AnyValue) GetBytesValue() []byte {
74 | if x, ok := m.Val.(*AnyValueBytes); ok {
75 | return x.BytesValue
76 | }
77 | return nil
78 | }
79 |
--------------------------------------------------------------------------------
/otel/omodel/compact_info_collect_visitor.go:
--------------------------------------------------------------------------------
1 | package omodel
2 |
3 | type traceInfoCollectVisitor struct {
4 | countVisitor *sameResourceCountVisitor
5 | serviceConsumedTime map[string]uint64
6 | }
7 |
8 | func (v *traceInfoCollectVisitor) Enter(span *Span) {
9 | v.countVisitor.Enter(span)
10 | v.serviceConsumedTime[span.ServiceName] += span.CalcConsumedTime()
11 | }
12 |
13 | func (v *traceInfoCollectVisitor) Leave(span *Span) {
14 | v.countVisitor.Leave(span)
15 | }
16 |
17 | func (v *traceInfoCollectVisitor) GetMaxResourceAccessCount() int {
18 | return v.countVisitor.GetMax()
19 | }
20 |
21 | func (v *traceInfoCollectVisitor) GetMostTimeConsumedService() string {
22 | maxService := ""
23 | maxTime := uint64(0)
24 |
25 | for s, t := range v.serviceConsumedTime {
26 | if maxTime < t {
27 | maxService = s
28 | maxTime = t
29 | }
30 | }
31 |
32 | return maxService
33 | }
34 |
--------------------------------------------------------------------------------
/otel/omodel/same_resource_count_visitor.go:
--------------------------------------------------------------------------------
1 | package omodel
2 |
3 | import (
4 | "github.com/samber/lo"
5 | )
6 |
7 | type sameResourceCountVisitor struct {
8 | visitedCounts map[string]int
9 | }
10 |
11 | func (v *sameResourceCountVisitor) Enter(span *Span) {
12 | service := span.ServiceName
13 | v.visitedCounts[service] += 1
14 | }
15 |
16 | func (v *sameResourceCountVisitor) Leave(*Span) {}
17 |
18 | func (v *sameResourceCountVisitor) GetMax() int {
19 | return lo.Max(lo.Values(v.visitedCounts))
20 | }
21 |
--------------------------------------------------------------------------------
/otel/omodel/trace_compacter.go:
--------------------------------------------------------------------------------
1 | package omodel
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/lib"
6 | )
7 |
8 | type traceCompacter struct {
9 | parentStack *lib.Stack[Span]
10 | }
11 |
12 | func newTraceCompacter(top *Span) *traceCompacter {
13 | return &traceCompacter{
14 | parentStack: lib.NewStack[Span](top),
15 | }
16 | }
17 |
18 | func (c *traceCompacter) Enter(span *Span) {
19 | if !c.remains(span) {
20 | return
21 | }
22 |
23 | newParent := span.ShallowCopyWithoutDependency()
24 | if span.IsLastDbSpan() {
25 | newParent.ServiceName = fmt.Sprintf("%s (%s)", span.GetDBSystem(), newParent.ServiceName)
26 | }
27 |
28 | parent := c.parentStack.Top()
29 | if parent != nil {
30 | parent.Children = append(parent.Children, newParent)
31 | }
32 |
33 | c.parentStack.Push(newParent)
34 | }
35 |
36 | func (c *traceCompacter) Leave(span *Span) {
37 | if !c.parentStack.Top().IsSame(span) {
38 | return
39 | }
40 |
41 | c.parentStack.Pop()
42 | }
43 |
44 | func (c *traceCompacter) remains(span *Span) bool {
45 | if span.Parent == nil {
46 | return true
47 | }
48 | if span.Parent.ServiceName != span.ServiceName {
49 | return true
50 | }
51 | if span.IsLastDbSpan() {
52 | return true
53 | }
54 |
55 | return false
56 | }
57 |
--------------------------------------------------------------------------------
/otel/omodel/trace_tree.go:
--------------------------------------------------------------------------------
1 | package omodel
2 |
3 | import (
4 | "fmt"
5 | "github.com/mrasu/GravityR/html/viewmodel"
6 | )
7 |
8 | type TraceTree struct {
9 | Root *Span
10 | }
11 |
12 | func NewTraceTree(root *Span) *TraceTree {
13 | return &TraceTree{Root: root}
14 | }
15 |
16 | func (tt *TraceTree) visit(v traceVisitor) {
17 | tt.visitRecursive(v, tt.Root)
18 | }
19 |
20 | func (tt *TraceTree) visitRecursive(v traceVisitor, current *Span) {
21 | v.Enter(current)
22 | for _, c := range current.Children {
23 | tt.visitRecursive(v, c)
24 | }
25 | v.Leave(current)
26 | }
27 |
28 | // Compact returns new TraceTree having only the first traces of services and the last traces going to DB which doesn't using otel.
29 | func (tt *TraceTree) Compact() *TraceTree {
30 | top := &Span{}
31 | tt.visit(newTraceCompacter(top))
32 |
33 | if len(top.Children) > 0 {
34 | return &TraceTree{Root: top.Children[0]}
35 | } else {
36 | return &TraceTree{Root: nil}
37 | }
38 | }
39 |
40 | func (tt *TraceTree) MaxSameServiceAccessCount() int {
41 | v := &sameResourceCountVisitor{
42 | visitedCounts: map[string]int{},
43 | }
44 | tt.visit(v)
45 |
46 | return v.GetMax()
47 | }
48 |
49 | func (tt *TraceTree) ToCompactViewModel() *viewmodel.VmOtelCompactTrace {
50 | v := &traceInfoCollectVisitor{
51 | countVisitor: &sameResourceCountVisitor{
52 | visitedCounts: map[string]int{},
53 | },
54 | serviceConsumedTime: map[string]uint64{},
55 | }
56 | tt.visit(v)
57 |
58 | return &viewmodel.VmOtelCompactTrace{
59 | TraceId: fmt.Sprintf("%x", tt.Root.TraceId),
60 | SameServiceAccessCount: v.GetMaxResourceAccessCount(),
61 | TimeConsumingServiceName: v.GetMostTimeConsumedService(),
62 | Root: tt.Root.ToViewModel(),
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/otel/omodel/trace_visitor.go:
--------------------------------------------------------------------------------
1 | package omodel
2 |
3 | type traceVisitor interface {
4 | Enter(*Span)
5 | Leave(*Span)
6 | }
7 |
--------------------------------------------------------------------------------
/thelper/bytes.go:
--------------------------------------------------------------------------------
1 | package thelper
2 |
3 | import (
4 | "encoding/hex"
5 | "github.com/stretchr/testify/require"
6 | "testing"
7 | )
8 |
9 | func DecodeHex(t *testing.T, h string) []byte {
10 | t.Helper()
11 |
12 | res, err := hex.DecodeString(h)
13 | require.NoError(t, err)
14 | return res
15 | }
16 |
--------------------------------------------------------------------------------
/thelper/db.go:
--------------------------------------------------------------------------------
1 | package thelper
2 |
3 | import (
4 | "github.com/DATA-DOG/go-sqlmock"
5 | "github.com/jmoiron/sqlx"
6 | "github.com/mrasu/GravityR/infra/mysql"
7 | "github.com/mrasu/GravityR/infra/postgres"
8 | "github.com/stretchr/testify/assert"
9 | "testing"
10 |
11 | _ "github.com/go-sql-driver/mysql"
12 | )
13 |
14 | func MockMysqlDB(t *testing.T, f func(*mysql.DB, sqlmock.Sqlmock)) {
15 | mockDB(t, "mysql", func(db *sqlx.DB, mock sqlmock.Sqlmock) {
16 | f(mysql.NewDB(db), mock)
17 | })
18 | }
19 |
20 | func MockPostgresDB(t *testing.T, f func(*postgres.DB, sqlmock.Sqlmock)) {
21 | mockDB(t, "postgres", func(db *sqlx.DB, mock sqlmock.Sqlmock) {
22 | f(postgres.NewDB(db), mock)
23 | })
24 | }
25 |
26 | func mockDB(t *testing.T, driver string, f func(*sqlx.DB, sqlmock.Sqlmock)) {
27 | t.Helper()
28 | mockdb, mock, err := sqlmock.New()
29 | assert.NoError(t, err)
30 | defer mockdb.Close()
31 | db := sqlx.NewDb(mockdb, driver)
32 | f(db, mock)
33 | assert.NoError(t, mock.ExpectationsWereMet())
34 | }
35 |
--------------------------------------------------------------------------------
/thelper/file.go:
--------------------------------------------------------------------------------
1 | package thelper
2 |
3 | import (
4 | "github.com/stretchr/testify/require"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 | "testing"
9 | )
10 |
11 | func ReadFromFiles(t *testing.T, filename string) string {
12 | t.Helper()
13 |
14 | _, b, _, _ := runtime.Caller(0)
15 | p := filepath.Join(filepath.Dir(b), "../", "./testdata/files/", filename)
16 | txt, err := os.ReadFile(p)
17 | require.NoError(t, err)
18 |
19 | return string(txt)
20 | }
21 |
--------------------------------------------------------------------------------
/thelper/injection.go:
--------------------------------------------------------------------------------
1 | package thelper
2 |
3 | import (
4 | "github.com/mrasu/GravityR/injection"
5 | "testing/fstest"
6 | )
7 |
8 | func InjectClientDist() {
9 | injection.ClientDist = fstest.MapFS{
10 | "client/dist/assets/main.js": {
11 | Data: []byte("console.log('hello')"),
12 | },
13 | "client/dist/assets/main.css": {
14 | Data: []byte("body{}"),
15 | },
16 | "client/dist/assets/mermaid.js": {
17 | Data: []byte("console.log('hello')"),
18 | },
19 | "client/dist/assets/mermaid.css": {
20 | Data: []byte("body{}"),
21 | },
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/thelper/jaeger.go:
--------------------------------------------------------------------------------
1 | package thelper
2 |
3 | import (
4 | "github.com/jaegertracing/jaeger/cmd/query/app/apiv3"
5 | "github.com/jaegertracing/jaeger/cmd/query/app/querysvc"
6 | "github.com/jaegertracing/jaeger/proto-gen/api_v3"
7 | common "github.com/jaegertracing/jaeger/proto-gen/otel/common/v1"
8 | v1 "github.com/jaegertracing/jaeger/proto-gen/otel/resource/v1"
9 | "github.com/jaegertracing/jaeger/storage/spanstore"
10 | "github.com/stretchr/testify/require"
11 | semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
12 | "google.golang.org/grpc"
13 | "net"
14 | "testing"
15 | )
16 |
17 | func UseGRPCServer(t *testing.T, reader spanstore.Reader, fn func(net.Addr)) {
18 | t.Helper()
19 |
20 | s := grpc.NewServer()
21 | qs := querysvc.NewQueryService(
22 | reader, nil, querysvc.QueryServiceOptions{},
23 | )
24 | api_v3.RegisterQueryServiceServer(s, &apiv3.Handler{QueryService: qs})
25 | lis, err := net.Listen("tcp", ":0")
26 | require.NoError(t, err)
27 | go func() {
28 | err := s.Serve(lis)
29 | require.NoError(t, err)
30 | }()
31 | defer s.Stop()
32 |
33 | fn(lis.Addr())
34 | }
35 |
36 | func BuildJaegerResource(serviceName string) *v1.Resource {
37 | return &v1.Resource{
38 | Attributes: []*common.KeyValue{
39 | {
40 | Key: string(semconv.ServiceNameKey),
41 | Value: &common.AnyValue{
42 | Value: &common.AnyValue_StringValue{StringValue: serviceName},
43 | },
44 | },
45 | },
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/thelper/setup.go:
--------------------------------------------------------------------------------
1 | package thelper
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | var DBName string
8 | var MySQLDsn string
9 |
10 | func SetUp() {
11 | DBName = "gravityr"
12 | MySQLDsn = fmt.Sprintf("%s@/%s", "root", DBName)
13 | }
14 |
--------------------------------------------------------------------------------
/thelper/temp.go:
--------------------------------------------------------------------------------
1 | package thelper
2 |
3 | import (
4 | "github.com/stretchr/testify/require"
5 | "os"
6 | "testing"
7 | )
8 |
9 | func CreateTemp(t *testing.T, pattern string, fn func(*os.File)) {
10 | t.Helper()
11 |
12 | tmpfile, err := os.CreateTemp("", pattern)
13 | require.NoError(t, err)
14 | defer os.Remove(tmpfile.Name())
15 |
16 | fn(tmpfile)
17 | }
18 |
--------------------------------------------------------------------------------