diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ec417a851b..744688a733 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install dependencies run: cd frontend && yarn install - name: Run ESLint @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Run tests shell: bash run: | @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build EE query-service image shell: bash run: | diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 404cc66f7b..14a0c127aa 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -39,11 +39,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -68,4 +68,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/create-issue-on-pr-merge.yml b/.github/workflows/create-issue-on-pr-merge.yml index 12910628ed..2b0c849ffa 100644 --- a/.github/workflows/create-issue-on-pr-merge.yml +++ b/.github/workflows/create-issue-on-pr-merge.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Codebase - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: signoz/gh-bot - name: Use Node v16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: 16 - name: Setup Cache & Install Dependencies diff --git a/.github/workflows/e2e-k3s.yaml b/.github/workflows/e2e-k3s.yaml index a1a307a9d9..8eab9d9beb 100644 --- a/.github/workflows/e2e-k3s.yaml +++ b/.github/workflows/e2e-k3s.yaml @@ -13,7 +13,7 @@ jobs: DOCKER_TAG: pull-${{ github.event.number }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build query-service image env: @@ -69,12 +69,14 @@ jobs: --restart='OnFailure' -i --rm --command -- curl -X POST -F \ 'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm - - name: Get short commit SHA and display tunnel URL + - name: Get short commit SHA, display tunnel URL and IP Address of the worker node id: get-subdomain run: | subdomain="pr-$(git rev-parse --short HEAD)" echo "URL for tunnelling: https://$subdomain.loca.lt" - echo "::set-output name=subdomain::$subdomain" + echo "subdomain=$subdomain" >> $GITHUB_OUTPUT + worker_ip="$(curl -4 -s ipconfig.io/ip)" + echo "Worker node IP address: $worker_ip" - name: Start tunnel env: diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index 0a6addfaeb..d6c05dfd6f 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -9,8 +9,8 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: "16.x" - name: Install dependencies diff --git a/.github/workflows/pr_verify_linked_issue.yml b/.github/workflows/pr_verify_linked_issue.yml index a2442cc3a4..927b46c216 100644 --- a/.github/workflows/pr_verify_linked_issue.yml +++ b/.github/workflows/pr_verify_linked_issue.yml @@ -14,6 +14,6 @@ jobs: name: Ensure Pull Request has a linked issue. steps: - name: Verify Linked Issue - uses: srikanthccv/verify-linked-issue-action@v0.70 + uses: srikanthccv/verify-linked-issue-action@v0.71 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index b497db5001..c198442ae5 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -14,19 +14,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 with: version: latest - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - uses: benjlevesque/short-sha@v1.2 + - uses: benjlevesque/short-sha@v2.2 id: short-sha - name: Get branch name id: branch-name @@ -49,19 +49,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 with: version: latest - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - uses: benjlevesque/short-sha@v1.2 + - uses: benjlevesque/short-sha@v2.2 id: short-sha - name: Get branch name id: branch-name @@ -84,7 +84,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install dependencies working-directory: frontend run: yarn install @@ -97,15 +97,15 @@ jobs: run: npm run lint continue-on-error: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 with: version: latest - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - uses: benjlevesque/short-sha@v1.2 + - uses: benjlevesque/short-sha@v2.2 id: short-sha - name: Get branch name id: branch-name diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 678007f91a..cb8189b134 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -12,6 +12,12 @@ on: jobs: update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write runs-on: ubuntu-latest steps: # (Optional) GitHub Enterprise requires GHE_HOST variable set diff --git a/.github/workflows/remove-label.yaml b/.github/workflows/remove-label.yaml index 418475ea23..ef570a6ac1 100644 --- a/.github/workflows/remove-label.yaml +++ b/.github/workflows/remove-label.yaml @@ -8,9 +8,15 @@ jobs: remove: runs-on: ubuntu-latest steps: - - name: Remove label - uses: buildsville/add-remove-label@v1 + - name: Remove label ok-to-test from PR + uses: buildsville/add-remove-label@v2.0.0 with: - label: ok-to-test,testing-deploy + label: ok-to-test + type: remove + token: ${{ secrets.GITHUB_TOKEN }} + - name: Remove label testing-deploy from PR + uses: buildsville/add-remove-label@v2.0.0 + with: + label: testing-deploy type: remove token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index c19c4f5452..742768525f 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -3,7 +3,7 @@ on: pull_request: branches: - main - - v* + - develop paths: - 'frontend/**' defaults: @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Sonar analysis diff --git a/Makefile b/Makefile index c08b0c5f85..bef8c8bce7 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ build-push-frontend: @echo "--> Building and pushing frontend docker image" @echo "------------------" @cd $(FRONTEND_DIRECTORY) && \ - docker buildx build --file Dockerfile --progress plane --push --platform linux/arm64,linux/amd64 \ + docker buildx build --file Dockerfile --progress plain --push --platform linux/arm64,linux/amd64 \ --tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) . # Steps to build and push docker image of query service @@ -73,7 +73,7 @@ build-push-query-service: @echo "------------------" @echo "--> Building and pushing query-service docker image" @echo "------------------" - @docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plane \ + @docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plain \ --push --platform linux/arm64,linux/amd64 --build-arg LD_FLAGS="$(LD_FLAGS)" \ --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) . @@ -98,7 +98,7 @@ build-push-ee-query-service: @echo "--> Building and pushing query-service docker image" @echo "------------------" @docker buildx build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \ - --progress plane --push --platform linux/arm64,linux/amd64 \ + --progress plain --push --platform linux/arm64,linux/amd64 \ --build-arg LD_FLAGS="$(LD_FLAGS)" --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) . dev-setup: @@ -136,9 +136,18 @@ clear-swarm-data: @docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \ sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*" +clear-standalone-ch: + @docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \ + sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*" + +clear-swarm-ch: + @docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \ + sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*" + test: go test ./pkg/query-service/app/metrics/... go test ./pkg/query-service/cache/... go test ./pkg/query-service/app/... + go test ./pkg/query-service/app/querier/... go test ./pkg/query-service/converter/... go test ./pkg/query-service/formatter/... diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 2af03d4255..fec809c500 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -137,7 +137,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.20.2 + image: signoz/query-service:0.21.0 command: ["-config=/root/config/prometheus.yml"] # ports: # - "6060:6060" # pprof port @@ -166,7 +166,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:0.20.2 + image: signoz/frontend:0.21.0 deploy: restart_policy: condition: on-failure @@ -179,7 +179,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.76.1 + image: signoz/signoz-otel-collector:0.79.1 command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] user: root # required for reading docker container logs volumes: @@ -208,7 +208,7 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:0.76.1 + image: signoz/signoz-otel-collector:0.79.1 command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index e7b598ae38..84d2bdcd93 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -41,7 +41,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` otel-collector: container_name: otel-collector - image: signoz/signoz-otel-collector:0.76.1 + image: signoz/signoz-otel-collector:0.79.1 command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] # user: root # required for reading docker container logs volumes: @@ -67,7 +67,7 @@ services: otel-collector-metrics: container_name: otel-collector-metrics - image: signoz/signoz-otel-collector:0.76.1 + image: signoz/signoz-otel-collector:0.79.1 command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index a270892a88..70441af123 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -153,7 +153,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` query-service: - image: signoz/query-service:${DOCKER_TAG:-0.20.2} + image: signoz/query-service:${DOCKER_TAG:-0.21.0} container_name: query-service command: ["-config=/root/config/prometheus.yml"] # ports: @@ -181,7 +181,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.20.2} + image: signoz/frontend:${DOCKER_TAG:-0.21.0} container_name: frontend restart: on-failure depends_on: @@ -193,7 +193,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1} command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] user: root # required for reading docker container logs volumes: @@ -219,7 +219,7 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1} command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 315a211f9f..942ed24ced 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -277,7 +277,7 @@ func loggingMiddleware(next http.Handler) http.Handler { path, _ := route.GetPathTemplate() startTime := time.Now() next.ServeHTTP(w, r) - zap.S().Info(path, "\ttimeTaken: ", time.Now().Sub(startTime)) + zap.L().Info(path+"\ttimeTaken:"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path)) }) } @@ -289,7 +289,7 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler { path, _ := route.GetPathTemplate() startTime := time.Now() next.ServeHTTP(w, r) - zap.S().Info(path, "\tprivatePort: true", "\ttimeTaken: ", time.Now().Sub(startTime)) + zap.L().Info(path+"\tprivatePort: true \ttimeTaken"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path), zap.Bool("tprivatePort", true)) }) } diff --git a/ee/query-service/main.go b/ee/query-service/main.go index e29b86797a..6d38fb9f65 100644 --- a/ee/query-service/main.go +++ b/ee/query-service/main.go @@ -3,25 +3,73 @@ package main import ( "context" "flag" + "log" "os" "os/signal" + "strconv" "syscall" + "time" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" "go.signoz.io/signoz/ee/query-service/app" "go.signoz.io/signoz/pkg/query-service/auth" + "go.signoz.io/signoz/pkg/query-service/constants" baseconst "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/version" + "google.golang.org/grpc" + + zapotlpencoder "github.com/SigNoz/zap_otlp/zap_otlp_encoder" + zapotlpsync "github.com/SigNoz/zap_otlp/zap_otlp_sync" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) -func initZapLog() *zap.Logger { +func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger { config := zap.NewDevelopmentConfig() + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + config.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder + otlpEncoder := zapotlpencoder.NewOTLPEncoder(config.EncoderConfig) + consoleEncoder := zapcore.NewConsoleEncoder(config.EncoderConfig) + defaultLogLevel := zapcore.DebugLevel config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder config.EncoderConfig.TimeKey = "timestamp" config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - logger, _ := config.Build() + + res := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("query-service"), + ) + + core := zapcore.NewTee( + zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel), + ) + + if enableQueryServiceLogOTLPExport == true { + conn, err := grpc.DialContext(ctx, constants.OTLPTarget, grpc.WithBlock(), grpc.WithInsecure(), grpc.WithTimeout(time.Second*30)) + if err != nil { + log.Println("failed to connect to otlp collector to export query service logs with error:", err) + } else { + logExportBatchSizeInt, err := strconv.Atoi(baseconst.LogExportBatchSize) + if err != nil { + logExportBatchSizeInt = 1000 + } + ws := zapcore.AddSync(zapotlpsync.NewOtlpSyncer(conn, zapotlpsync.Options{ + BatchSize: logExportBatchSizeInt, + ResourceSchema: semconv.SchemaURL, + Resource: res, + })) + core = zapcore.NewTee( + zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel), + zapcore.NewCore(otlpEncoder, zapcore.NewMultiWriteSyncer(ws), defaultLogLevel), + ) + } + } + logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) + return logger } @@ -34,12 +82,15 @@ func main() { // the url used to build link in the alert messages in slack and other systems var ruleRepoURL string + var enableQueryServiceLogOTLPExport bool + flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)") flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)") flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)") + flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)") flag.Parse() - loggerMgr := initZapLog() + loggerMgr := initZapLog(enableQueryServiceLogOTLPExport) zap.ReplaceGlobals(loggerMgr) defer loggerMgr.Sync() // flushes buffer, if any diff --git a/frontend/CONTRIBUTIONS.md b/frontend/CONTRIBUTIONS.md new file mode 100644 index 0000000000..0b3d76219c --- /dev/null +++ b/frontend/CONTRIBUTIONS.md @@ -0,0 +1,56 @@ +# **Frontend Guidelines** + +Embrace the spirit of collaboration and contribute to the success of our open-source project by adhering to these frontend development guidelines with precision and passion. + +### React and Components + +- Strive to create small and modular components, ensuring they are divided into individual pieces for improved maintainability and reusability. +- Avoid passing inline objects or functions as props to React components, as they are recreated with each render cycle. + Utilize careful memoization of functions and variables, balancing optimization efforts to prevent potential performance issues. [When to useMemo and useCallback](https://kentcdodds.com/blog/usememo-and-usecallback) by Kent C. Dodds is quite helpful for this scenario. +- Minimize the use of inline functions whenever possible to enhance code readability and improve overall comprehension. +- Employ the appropriate usage of useMemo and useCallback hooks for effective memoization of values and functions. +- Determine the appropriate placement of components: + - Pages should contain an aggregation of all components and containers. + - Commonly used components should reside in the 'components' directory. + - Parent components responsible for data manipulation should be placed in the 'container' directory. +- Strategically decide where to store data, either in global state or local components: + - Begin by storing data in local components and gradually transition to global state as necessary. +- Avoid importing default namespace `React` as the project is using `v18` and `import React from 'react'` is not needed anymore. +- When a function requires more than three arguments (except when memoized), encapsulate them within an object to enhance readability and reduce potential parameter complexity. + +### API and Services + +- Avoid incorporating business logic within API/Service files to maintain flexibility for consumers to handle it according to their specific needs. +- Employ the use of the useQuery hook for fetching data and the useMutation hook for updating data, ensuring a consistent and efficient approach. +- Utilize the useQueryClient hook when updating the cache, facilitating smooth and effective management of data within the application. + +**Note -** In our project, we are utilizing React Query v3. To gain a comprehensive understanding of its features and implementation, we recommend referring to the [official documentation](https://tanstack.com/query/v3/docs/react/overview) as a valuable resource. + +### Styling + +- Refrain from using inline styling within React components to maintain separation of concerns and promote a more maintainable codebase. +- Opt for using the rem unit instead of px values to ensure better scalability and responsiveness across different devices and screen sizes. + +### Linting and Setup + +- It is crucial to refrain from disabling ESLint and TypeScript errors within the project. If there is a specific rule that needs to be disabled, provide a clear and justified explanation for doing so. Maintaining the integrity of the linting and type-checking processes ensures code quality and consistency throughout the codebase. +- In our project, we rely on several essential ESLint plugins, namely: + - [plugin:@typescript-eslint](https://typescript-eslint.io/rules/) + - [airbnb styleguide](https://github.com/airbnb/javascript) + - [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) + + To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project. + +### Naming Conventions + +- Ensure that component names are written in Capital Case, while the folder names should be in lowercase. +- Keep all other elements, such as variables, functions, and file names, in lowercase. + +### Miscellaneous + +- Ensure that functions are modularized and follow the Single Responsibility Principle (SRP). The function's name should accurately convey its purpose and functionality. +- Semantic division of functions into smaller units should be prioritized for improved readability and maintainability. + Aim to keep functions concise and avoid exceeding a maximum length of 40 lines to enhance code understandability and ease of maintenance. +- Eliminate the use of hard-coded strings or enums, favoring a more flexible and maintainable approach. +- Strive to internationalize all strings within the codebase to support localization and improve accessibility for users across different languages. +- Minimize the usage of multiple if statements or switch cases within a function. Consider creating a mapper and separating logic into multiple functions for better code organization. diff --git a/frontend/public/locales/en-GB/trace.json b/frontend/public/locales/en-GB/trace.json new file mode 100644 index 0000000000..587f565a21 --- /dev/null +++ b/frontend/public/locales/en-GB/trace.json @@ -0,0 +1,11 @@ +{ + "options_menu": { + "options": "Options", + "format": "Format", + "row": "Row", + "default": "Default", + "column": "Column", + "maxLines": "Max lines per Row", + "addColumn": "Add a column" + } +} diff --git a/frontend/public/locales/en/trace.json b/frontend/public/locales/en/trace.json new file mode 100644 index 0000000000..587f565a21 --- /dev/null +++ b/frontend/public/locales/en/trace.json @@ -0,0 +1,11 @@ +{ + "options_menu": { + "options": "Options", + "format": "Format", + "row": "Row", + "default": "Default", + "column": "Column", + "maxLines": "Max lines per Row", + "addColumn": "Add a column" + } +} diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 0b241fa121..a4d6b89c16 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -15,6 +15,11 @@ export const ServiceMapPage = Loadable( () => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'), ); +export const TracesExplorer = Loadable( + () => + import(/* webpackChunkName: "Traces Explorer Page" */ 'pages/TracesExplorer'), +); + export const TraceFilter = Loadable( () => import(/* webpackChunkName: "Trace Filter Page" */ 'pages/Trace'), ); @@ -101,6 +106,10 @@ export const Logs = Loadable( () => import(/* webpackChunkName: "Logs" */ 'pages/Logs'), ); +export const LogsExplorer = Loadable( + () => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'), +); + export const Login = Loadable( () => import(/* webpackChunkName: "Login" */ 'pages/Login'), ); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 7210fd5928..49d48de066 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -16,6 +16,7 @@ import { ListAllALertsPage, Login, Logs, + LogsExplorer, MySettings, NewDashboardPage, OrganizationSettings, @@ -29,6 +30,7 @@ import { StatusPage, TraceDetail, TraceFilter, + TracesExplorer, UnAuthorized, UsageExplorerPage, } from './pageComponents'; @@ -139,6 +141,13 @@ const routes: AppRoutes[] = [ isPrivate: true, key: 'TRACE', }, + { + path: ROUTES.TRACES_EXPLORER, + exact: true, + component: TracesExplorer, + isPrivate: true, + key: 'TRACES_EXPLORER', + }, { path: ROUTES.CHANNELS_NEW, exact: true, @@ -209,6 +218,13 @@ const routes: AppRoutes[] = [ key: 'LOGS', isPrivate: true, }, + { + path: ROUTES.LOGS_EXPLORER, + exact: true, + component: LogsExplorer, + key: 'LOGS_EXPLORER', + isPrivate: true, + }, { path: ROUTES.LOGIN, exact: true, diff --git a/frontend/src/api/ErrorResponseHandler.ts b/frontend/src/api/ErrorResponseHandler.ts index 060b93493f..2c42f0951b 100644 --- a/frontend/src/api/ErrorResponseHandler.ts +++ b/frontend/src/api/ErrorResponseHandler.ts @@ -16,7 +16,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse { return { statusCode, payload: null, - error: 'Not Found', + error: data.errorType, message: null, }; } diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index a31397dd3c..0ac5de8030 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -1,5 +1,6 @@ // ** Helpers import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; import { BaseAutocompleteData, @@ -18,6 +19,7 @@ import { EQueryType } from 'types/common/dashboard'; import { BoolOperators, DataSource, + LogsAggregatorOperator, MetricAggregateOperator, NumberOperators, PanelTypeKeys, @@ -25,6 +27,7 @@ import { QueryBuilderData, ReduceOperators, StringOperators, + TracesAggregatorOperator, } from 'types/common/queryBuilder'; import { SelectOption } from 'types/common/select'; import { v4 as uuid } from 'uuid'; @@ -100,14 +103,17 @@ export const initialHavingValues: HavingForm = { }; export const initialAutocompleteData: BaseAutocompleteData = { - id: uuid(), + id: createIdFromObjectFields( + { dataType: null, key: '', isColumn: null, type: null }, + baseAutoCompleteIdKeysOrder, + ), dataType: null, key: '', isColumn: null, type: null, }; -export const initialQueryBuilderFormValues: IBuilderQuery = { +const initialQueryBuilderFormValues: IBuilderQuery = { dataSource: DataSource.METRICS, queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), aggregateOperator: MetricAggregateOperator.NOOP, @@ -127,6 +133,27 @@ export const initialQueryBuilderFormValues: IBuilderQuery = { reduceTo: 'sum', }; +const initialQueryBuilderFormLogsValues: IBuilderQuery = { + ...initialQueryBuilderFormValues, + aggregateOperator: LogsAggregatorOperator.COUNT, + dataSource: DataSource.LOGS, +}; + +const initialQueryBuilderFormTracesValues: IBuilderQuery = { + ...initialQueryBuilderFormValues, + aggregateOperator: TracesAggregatorOperator.COUNT, + dataSource: DataSource.TRACES, +}; + +export const initialQueryBuilderFormValuesMap: Record< + DataSource, + IBuilderQuery +> = { + metrics: initialQueryBuilderFormValues, + logs: initialQueryBuilderFormLogsValues, + traces: initialQueryBuilderFormTracesValues, +}; + export const initialFormulaBuilderFormValues: IBuilderFormula = { queryName: createNewBuilderItemName({ existNames: [], @@ -161,17 +188,39 @@ export const initialSingleQueryMap: Record< IClickHouseQuery | IPromQLQuery > = { clickhouse_sql: initialClickHouseData, promql: initialQueryPromQLData }; -export const initialQuery: QueryState = { +export const initialQueryState: QueryState = { + id: uuid(), builder: initialQueryBuilderData, clickhouse_sql: [initialClickHouseData], promql: [initialQueryPromQLData], }; -export const initialQueryWithType: Query = { - ...initialQuery, +const initialQueryWithType: Query = { + ...initialQueryState, queryType: EQueryType.QUERY_BUILDER, }; +const initialQueryLogsWithType: Query = { + ...initialQueryWithType, + builder: { + ...initialQueryWithType.builder, + queryData: [initialQueryBuilderFormValuesMap.logs], + }, +}; +const initialQueryTracesWithType: Query = { + ...initialQueryWithType, + builder: { + ...initialQueryWithType.builder, + queryData: [initialQueryBuilderFormValuesMap.traces], + }, +}; + +export const initialQueriesMap: Record = { + metrics: initialQueryWithType, + logs: initialQueryLogsWithType, + traces: initialQueryTracesWithType, +}; + export const operatorsByTypes: Record = { string: Object.values(StringOperators), number: Object.values(NumberOperators), diff --git a/frontend/src/constants/queryBuilderQueryNames.ts b/frontend/src/constants/queryBuilderQueryNames.ts index cf668b7ef3..5a9e9dbfe9 100644 --- a/frontend/src/constants/queryBuilderQueryNames.ts +++ b/frontend/src/constants/queryBuilderQueryNames.ts @@ -1 +1,2 @@ export const COMPOSITE_QUERY = 'compositeQuery'; +export const PANEL_TYPES_QUERY = 'panelTypes'; diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 7cc38c153e..c4997dd45b 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -1,3 +1,5 @@ export const REACT_QUERY_KEY = { GET_ALL_LICENCES: 'GET_ALL_LICENCES', + GET_QUERY_RANGE: 'GET_QUERY_RANGE', + GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS', }; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index b5a92b3a0e..f911b6be57 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -5,6 +5,7 @@ const ROUTES = { SERVICE_MAP: '/service-map', TRACE: '/trace', TRACE_DETAIL: '/trace/:id', + TRACES_EXPLORER: '/traces-explorer', SETTINGS: '/settings', INSTRUMENTATION: '/get-started', USAGE_EXPLORER: '/usage-explorer', @@ -27,6 +28,7 @@ const ROUTES = { UN_AUTHORIZED: '/un-authorized', NOT_FOUND: '/not-found', LOGS: '/logs', + LOGS_EXPLORER: '/logs-explorer', HOME_PAGE: '/', PASSWORD_RESET: '/password-reset', LIST_LICENSES: '/licenses', diff --git a/frontend/src/constants/theme.ts b/frontend/src/constants/theme.ts index ce6cdd354a..c6c1b0b32b 100644 --- a/frontend/src/constants/theme.ts +++ b/frontend/src/constants/theme.ts @@ -36,8 +36,11 @@ const themeColors = { royalGrey: '#888888', matterhornGrey: '#555555', whiteCream: '#ffffffd5', + white: '#ffffff', black: '#000000', + lightBlack: '#141414', lightgrey: '#ddd', + lightWhite: '#ffffffd9', borderLightGrey: '#d9d9d9', borderDarkGrey: '#424242', }; diff --git a/frontend/src/container/Controls/config.ts b/frontend/src/container/Controls/config.ts new file mode 100644 index 0000000000..cc0378c546 --- /dev/null +++ b/frontend/src/container/Controls/config.ts @@ -0,0 +1,7 @@ +import { CSSProperties } from 'react'; + +export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200]; + +export const defaultSelectStyle: CSSProperties = { + minWidth: '6rem', +}; diff --git a/frontend/src/container/Controls/index.tsx b/frontend/src/container/Controls/index.tsx new file mode 100644 index 0000000000..a9a656bfc8 --- /dev/null +++ b/frontend/src/container/Controls/index.tsx @@ -0,0 +1,69 @@ +import { LeftOutlined, RightOutlined } from '@ant-design/icons'; +import { Button, Select } from 'antd'; +import { memo, useMemo } from 'react'; + +import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config'; +import { Container } from './styles'; + +interface ControlsProps { + count: number; + countPerPage: number; + isLoading: boolean; + handleNavigatePrevious: () => void; + handleNavigateNext: () => void; + handleCountItemsPerPageChange: (e: number) => void; +} + +function Controls(props: ControlsProps): JSX.Element | null { + const { + count, + isLoading, + countPerPage, + handleNavigatePrevious, + handleNavigateNext, + handleCountItemsPerPageChange, + } = props; + + const isNextAndPreviousDisabled = useMemo( + () => isLoading || countPerPage === 0 || count === 0 || count < countPerPage, + [isLoading, countPerPage, count], + ); + + return ( + + + + + + ); +} + +export default memo(Controls); diff --git a/frontend/src/container/Controls/styles.ts b/frontend/src/container/Controls/styles.ts new file mode 100644 index 0000000000..0407b8f790 --- /dev/null +++ b/frontend/src/container/Controls/styles.ts @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; +`; diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 523997c2a3..ce73dda62b 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -1,5 +1,5 @@ import { - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, initialQueryPromQLData, PANEL_TYPES, } from 'constants/queryBuilder'; @@ -11,11 +11,6 @@ import { defaultMatchType, } from 'types/api/alerts/def'; import { EQueryType } from 'types/common/dashboard'; -import { - DataSource, - LogsAggregatorOperator, - TracesAggregatorOperator, -} from 'types/common/queryBuilder'; const defaultAlertDescription = 'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})'; @@ -32,7 +27,7 @@ export const alertDefaults: AlertDef = { condition: { compositeQuery: { builderQueries: { - A: initialQueryBuilderFormValues, + A: initialQueryBuilderFormValuesMap.metrics, }, promQueries: { A: initialQueryPromQLData }, chQueries: { @@ -61,11 +56,7 @@ export const logAlertDefaults: AlertDef = { condition: { compositeQuery: { builderQueries: { - A: { - ...initialQueryBuilderFormValues, - aggregateOperator: LogsAggregatorOperator.COUNT, - dataSource: DataSource.LOGS, - }, + A: initialQueryBuilderFormValuesMap.logs, }, promQueries: { A: initialQueryPromQLData }, chQueries: { @@ -95,11 +86,7 @@ export const traceAlertDefaults: AlertDef = { condition: { compositeQuery: { builderQueries: { - A: { - ...initialQueryBuilderFormValues, - aggregateOperator: TracesAggregatorOperator.COUNT, - dataSource: DataSource.TRACES, - }, + A: initialQueryBuilderFormValuesMap.traces, }, promQueries: { A: initialQueryPromQLData }, chQueries: { @@ -129,11 +116,7 @@ export const exceptionAlertDefaults: AlertDef = { condition: { compositeQuery: { builderQueries: { - A: { - ...initialQueryBuilderFormValues, - aggregateOperator: TracesAggregatorOperator.COUNT, - dataSource: DataSource.TRACES, - }, + A: initialQueryBuilderFormValuesMap.traces, }, promQueries: { A: initialQueryPromQLData }, chQueries: { diff --git a/frontend/src/container/CreateAlertRule/index.tsx b/frontend/src/container/CreateAlertRule/index.tsx index b0d146206d..40145d324e 100644 --- a/frontend/src/container/CreateAlertRule/index.tsx +++ b/frontend/src/container/CreateAlertRule/index.tsx @@ -1,16 +1,11 @@ import { Form, Row } from 'antd'; -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import FormAlertRules from 'container/FormAlertRules'; -import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import useUrlQuery from 'hooks/useUrlQuery'; -import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { useState } from 'react'; import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertDef } from 'types/api/alerts/def'; import { alertDefaults, - ALERTS_VALUES_MAP, exceptionAlertDefaults, logAlertDefaults, traceAlertDefaults, @@ -18,18 +13,12 @@ import { import SelectAlertType from './SelectAlertType'; function CreateRules(): JSX.Element { - const [initValues, setInitValues] = useState(alertDefaults); + const [initValues, setInitValues] = useState(null); const [alertType, setAlertType] = useState( AlertTypes.METRICS_BASED_ALERT, ); const [formInstance] = Form.useForm(); - const urlQuery = useUrlQuery(); - - const compositeQuery = urlQuery.get(COMPOSITE_QUERY); - - const { redirectWithQueryBuilderData } = useQueryBuilder(); - const onSelectType = (typ: AlertTypes): void => { setAlertType(typ); switch (typ) { @@ -45,15 +34,9 @@ function CreateRules(): JSX.Element { default: setInitValues(alertDefaults); } - - const value = ALERTS_VALUES_MAP[typ].condition.compositeQuery; - - const compositeQuery = mapQueryDataFromApi(value); - - redirectWithQueryBuilderData(compositeQuery); }; - if (!compositeQuery) { + if (!initValues) { return ( diff --git a/frontend/src/container/ExportPanel/ExportPanel.tsx b/frontend/src/container/ExportPanel/ExportPanel.tsx new file mode 100644 index 0000000000..2cd24e25d1 --- /dev/null +++ b/frontend/src/container/ExportPanel/ExportPanel.tsx @@ -0,0 +1,113 @@ +import { Button, Typography } from 'antd'; +import createDashboard from 'api/dashboard/create'; +import getAll from 'api/dashboard/getAll'; +import axios from 'axios'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useNotifications } from 'hooks/useNotifications'; +import { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation, useQuery } from 'react-query'; + +import { ExportPanelProps } from '.'; +import { + DashboardSelect, + NewDashboardButton, + SelectWrapper, + Title, + Wrapper, +} from './styles'; +import { getSelectOptions } from './utils'; + +function ExportPanel({ onExport }: ExportPanelProps): JSX.Element { + const { notifications } = useNotifications(); + const { t } = useTranslation(['dashboard']); + + const [selectedDashboardId, setSelectedDashboardId] = useState( + null, + ); + + const { data, isLoading, refetch } = useQuery({ + queryFn: getAll, + queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS, + }); + + const { + mutate: createNewDashboard, + isLoading: createDashboardLoading, + } = useMutation(createDashboard, { + onSuccess: () => { + refetch(); + }, + onError: (error) => { + if (axios.isAxiosError(error)) { + notifications.error({ + message: error.message, + }); + } + }, + }); + + const options = useMemo(() => getSelectOptions(data?.payload || []), [data]); + + const handleExportClick = useCallback((): void => { + const currentSelectedDashboard = data?.payload?.find( + ({ uuid }) => uuid === selectedDashboardId, + ); + + onExport(currentSelectedDashboard || null); + }, [data, selectedDashboardId, onExport]); + + const handleSelect = useCallback( + (selectedDashboardValue: string): void => { + setSelectedDashboardId(selectedDashboardValue); + }, + [setSelectedDashboardId], + ); + + const handleNewDashboard = useCallback(async () => { + createNewDashboard({ + title: t('new_dashboard_title', { + ns: 'dashboard', + }), + uploadedGrafana: false, + }); + }, [t, createNewDashboard]); + + return ( + + Export Panel + + + + + + + + Or create dashboard with this panel - + + New Dashboard + + + + ); +} + +export default ExportPanel; diff --git a/frontend/src/container/ExportPanel/config.ts b/frontend/src/container/ExportPanel/config.ts new file mode 100644 index 0000000000..a70d996b79 --- /dev/null +++ b/frontend/src/container/ExportPanel/config.ts @@ -0,0 +1,9 @@ +export const MENU_KEY = { + EXPORT: 'export', + CREATE_ALERTS: 'create-alerts', +}; + +export const MENU_LABEL = { + EXPORT: 'Export Panel', + CREATE_ALERTS: 'Create Alerts', +}; diff --git a/frontend/src/container/ExportPanel/index.tsx b/frontend/src/container/ExportPanel/index.tsx new file mode 100644 index 0000000000..a1673b541c --- /dev/null +++ b/frontend/src/container/ExportPanel/index.tsx @@ -0,0 +1,70 @@ +import { Button, Dropdown, MenuProps, Modal } from 'antd'; +import { useCallback, useMemo, useState } from 'react'; +import { Dashboard } from 'types/api/dashboard/getAll'; + +import { MENU_KEY, MENU_LABEL } from './config'; +import ExportPanelContainer from './ExportPanel'; + +function ExportPanel({ onExport }: ExportPanelProps): JSX.Element { + const [isExport, setIsExport] = useState(false); + + const onModalToggle = useCallback((value: boolean) => { + setIsExport(value); + }, []); + + const onMenuClickHandler: MenuProps['onClick'] = useCallback( + (e: OnClickProps) => { + if (e.key === MENU_KEY.EXPORT) { + onModalToggle(true); + } + }, + [onModalToggle], + ); + + const menu: MenuProps = useMemo( + () => ({ + items: [ + { + key: MENU_KEY.EXPORT, + label: MENU_LABEL.EXPORT, + }, + { + key: MENU_KEY.CREATE_ALERTS, + label: MENU_LABEL.CREATE_ALERTS, + }, + ], + onClick: onMenuClickHandler, + }), + [onMenuClickHandler], + ); + + const onCancel = (value: boolean) => (): void => { + onModalToggle(value); + }; + + return ( + <> + + + + + + + + ); +} + +interface OnClickProps { + key: string; +} + +export interface ExportPanelProps { + onExport: (dashboard: Dashboard | null) => void; +} + +export default ExportPanel; diff --git a/frontend/src/container/ExportPanel/styles.ts b/frontend/src/container/ExportPanel/styles.ts new file mode 100644 index 0000000000..8aa3f2fde8 --- /dev/null +++ b/frontend/src/container/ExportPanel/styles.ts @@ -0,0 +1,33 @@ +import { Button, Select, SelectProps, Space, Typography } from 'antd'; +import { FunctionComponent } from 'react'; +import styled from 'styled-components'; + +export const DashboardSelect: FunctionComponent = styled( + Select, +)` + width: 100%; +`; + +export const SelectWrapper = styled(Space)` + width: 100%; + margin-bottom: 1rem; + + .ant-space-item:first-child { + width: 100%; + max-width: 20rem; + } +`; + +export const Wrapper = styled(Space)` + width: 100%; +`; + +export const NewDashboardButton = styled(Button)` + &&& { + padding: 0 0.125rem; + } +`; + +export const Title = styled(Typography.Text)` + font-size: 1rem; +`; diff --git a/frontend/src/container/ExportPanel/utils.ts b/frontend/src/container/ExportPanel/utils.ts new file mode 100644 index 0000000000..128de92324 --- /dev/null +++ b/frontend/src/container/ExportPanel/utils.ts @@ -0,0 +1,10 @@ +import { SelectProps } from 'antd'; +import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll'; + +export const getSelectOptions = ( + data: AllDashboardsData, +): SelectProps['options'] => + data.map(({ uuid, data }) => ({ + label: data.title, + value: uuid, + })); diff --git a/frontend/src/container/FormAlertRules/BasicInfo.tsx b/frontend/src/container/FormAlertRules/BasicInfo.tsx index f4d99126ec..595e6facd6 100644 --- a/frontend/src/container/FormAlertRules/BasicInfo.tsx +++ b/frontend/src/container/FormAlertRules/BasicInfo.tsx @@ -1,6 +1,7 @@ import { Form, Select } from 'antd'; import { useTranslation } from 'react-i18next'; import { AlertDef, Labels } from 'types/api/alerts/def'; +import { requireErrorMessage } from 'utils/form/requireErrorMessage'; import ChannelSelect from './ChannelSelect'; import LabelSelect from './labels'; @@ -54,7 +55,15 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element { - + { setAlertDef({ @@ -97,10 +106,10 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element { { + onSelectChannels={(preferredChannels): void => { setAlertDef({ ...alertDef, - preferredChannels: s, + preferredChannels, }); }} /> diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index c0732264e2..f08d22df96 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -1,16 +1,15 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import { StaticLineProps } from 'components/Graph'; import Spinner from 'components/Spinner'; -import { PANEL_TYPES } from 'constants/queryBuilder'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import GridGraphComponent from 'container/GridGraphComponent'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Time } from 'container/TopNav/DateTimeSelection/config'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import getChartData from 'lib/getChartData'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useQuery } from 'react-query'; -import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; @@ -18,7 +17,7 @@ import { ChartContainer, FailedMessageContainer } from './styles'; export interface ChartPreviewProps { name: string; - query: Query | undefined; + query: Query | null; graphType?: GRAPH_TYPES; selectedTime?: timePreferenceType; selectedInterval?: Time; @@ -26,9 +25,6 @@ export interface ChartPreviewProps { threshold?: number | undefined; userQueryKey?: string; } -interface QueryResponseError { - message?: string; -} function ChartPreview({ name, @@ -76,39 +72,30 @@ function ChartPreview({ } }, [query]); - const queryResponse = useQuery({ - queryKey: [ - 'chartPreview', - userQueryKey || JSON.stringify(query), - selectedInterval, - ], - queryFn: () => - GetMetricQueryRange({ - query: query || { - queryType: EQueryType.QUERY_BUILDER, - promql: [], - builder: { - queryFormulas: [], - queryData: [], - }, - clickhouse_sql: [], - }, - globalSelectedInterval: selectedInterval, - graphType, - selectedTime, - }), - retry: false, - enabled: canQuery, - }); + const queryResponse = useGetQueryRange( + { + query: query || initialQueriesMap.metrics, + globalSelectedInterval: selectedInterval, + graphType, + selectedTime, + }, + { + queryKey: [ + 'chartPreview', + userQueryKey || JSON.stringify(query), + selectedInterval, + ], + retry: false, + enabled: canQuery, + }, + ); const chartDataSet = queryResponse.isError ? null : getChartData({ queryData: [ { - queryData: queryResponse?.data?.payload?.data?.result - ? queryResponse?.data?.payload?.data?.result - : [], + queryData: queryResponse?.data?.payload?.data?.result ?? [], }, ], }); @@ -119,11 +106,12 @@ function ChartPreview({ {(queryResponse?.isError || queryResponse?.error) && ( {' '} - {(queryResponse?.error as QueryResponseError).message || - t('preview_chart_unexpected_error')} + {queryResponse.error.message || t('preview_chart_unexpected_error')} )} - {queryResponse.isLoading && } + {queryResponse.isLoading && ( + + )} {chartDataSet && !queryResponse.isError && ( ( + (state) => state.app, + ); + const handleQueryCategoryChange = (queryType: string): void => { - setQueryCategory(queryType as EQueryType); + featureResponse.refetch().then(() => { + setQueryCategory(queryType as EQueryType); + }); }; const renderPromqlUI = (): JSX.Element => ; @@ -38,10 +47,6 @@ function QuerySection({ /> ); - const handleRunQuery = (): void => { - runQuery(); - }; - const tabs = [ { label: t('tab_qb'), @@ -76,7 +81,7 @@ function QuerySection({ onChange={handleQueryCategoryChange} tabBarExtraContent={ - @@ -95,7 +100,7 @@ function QuerySection({ onChange={handleQueryCategoryChange} tabBarExtraContent={ - @@ -132,7 +137,7 @@ interface QuerySectionProps { queryCategory: EQueryType; setQueryCategory: (n: EQueryType) => void; alertType: AlertTypes; - runQuery: () => void; + runQuery: VoidFunction; } export default QuerySection; diff --git a/frontend/src/container/FormAlertRules/RuleOptions.tsx b/frontend/src/container/FormAlertRules/RuleOptions.tsx index 254dafa1a4..22238a941b 100644 --- a/frontend/src/container/FormAlertRules/RuleOptions.tsx +++ b/frontend/src/container/FormAlertRules/RuleOptions.tsx @@ -140,12 +140,14 @@ function RuleOptions({ {queryCategory === EQueryType.PROM ? renderPromRuleOptions() : renderThresholdRuleOpts()} - + + + ); diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 6ed81a93d8..9db261afd0 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -48,7 +48,12 @@ function FormAlertRules({ // init namespace for translations const { t } = useTranslation('alerts'); - const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); + const { + currentQuery, + stagedQuery, + handleRunQuery, + redirectWithQueryBuilderData, + } = useQueryBuilder(); // use query client const ruleCache = useQueryClient(); @@ -65,35 +70,14 @@ function FormAlertRules({ const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]); - // manualStagedQuery requires manual staging of query - // when user clicks run query button. Useful for clickhouse tab where - // run query button is provided. - const [manualStagedQuery, setManualStagedQuery] = useState(); - - // this use effect initiates staged query and - // other queries based on server data. - // useful when fetching of initial values (from api) - // is delayed - - const { compositeQuery } = useShareBuilderUrl({ defaultValue: sq }); + useShareBuilderUrl({ defaultValue: sq }); useEffect(() => { - if (compositeQuery && !manualStagedQuery) { - setManualStagedQuery(compositeQuery); - } setAlertDef(initialValue); - }, [ - initialValue, - initQuery, - redirectWithQueryBuilderData, - currentQuery, - manualStagedQuery, - compositeQuery, - ]); + }, [initialValue]); const onRunQuery = (): void => { - setManualStagedQuery(currentQuery); - redirectWithQueryBuilderData(currentQuery); + handleRunQuery(); }; const onCancelHandler = useCallback(() => { @@ -115,8 +99,6 @@ function FormAlertRules({ } const query: Query = { ...currentQuery, queryType: val }; - setManualStagedQuery(query); - redirectWithQueryBuilderData(query); }; const { notifications } = useNotifications(); @@ -201,10 +183,6 @@ function FormAlertRules({ const isFormValid = useCallback((): boolean => { if (!alertDef.alert || alertDef.alert === '') { - notifications.error({ - message: 'Error', - description: t('alertname_required'), - }); return false; } @@ -217,14 +195,7 @@ function FormAlertRules({ } return validateQBParams(); - }, [ - t, - validateQBParams, - validateChQueryParams, - alertDef, - validatePromParams, - notifications, - ]); + }, [validateQBParams, validateChQueryParams, alertDef, validatePromParams]); const preparePostData = (): AlertDef => { const postableAlert: AlertDef = { @@ -328,9 +299,7 @@ function FormAlertRules({ title: t('confirm_save_title'), centered: true, content, - onOk() { - saveRule(); - }, + onOk: saveRule, }); }, [t, saveRule, currentQuery]); @@ -381,7 +350,7 @@ function FormAlertRules({ headline={} name="" threshold={alertDef.condition?.target} - query={manualStagedQuery} + query={stagedQuery} selectedInterval={toChartInterval(alertDef.evalWindow)} /> ); @@ -391,7 +360,7 @@ function FormAlertRules({ headline={} name="Chart Preview" threshold={alertDef.condition?.target} - query={manualStagedQuery} + query={stagedQuery} /> ); @@ -400,23 +369,25 @@ function FormAlertRules({ headline={} name="Chart Preview" threshold={alertDef.condition?.target} - query={manualStagedQuery} + query={stagedQuery} selectedInterval={toChartInterval(alertDef.evalWindow)} /> ); const isNewRule = ruleId === 0; + const isAlertNameMissing = !formInstance.getFieldValue('alert'); + const isAlertAvialableToSave = isAlertAvialable && - isNewRule && - currentQuery.queryType === EQueryType.QUERY_BUILDER; + currentQuery.queryType === EQueryType.QUERY_BUILDER && + alertType !== AlertTypes.METRICS_BASED_ALERT; return ( <> {Element} - + } - disabled={isAlertAvialableToSave} + disabled={isAlertNameMissing || isAlertAvialableToSave} > {isNewRule ? t('button_createrule') : t('button_savechanges')} diff --git a/frontend/src/container/FormAlertRules/labels/index.tsx b/frontend/src/container/FormAlertRules/labels/index.tsx index ef7b94d01f..30583e12f9 100644 --- a/frontend/src/container/FormAlertRules/labels/index.tsx +++ b/frontend/src/container/FormAlertRules/labels/index.tsx @@ -84,8 +84,8 @@ function LabelSelect({ handleBlur(); }, [handleBlur]); - const handleChange = (e: ChangeEvent): void => { - setCurrentVal(e.target?.value); + const handleLabelChange = (event: ChangeEvent): void => { + setCurrentVal(event.target?.value.replace(':', '')); }; const handleClose = (key: string): void => { @@ -133,9 +133,9 @@ function LabelSelect({
{ - if (e.key === 'Enter' || e.code === 'Enter') { + if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') { send('NEXT'); } }} diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx index 04b35ee83b..1f5759f34b 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx @@ -7,16 +7,13 @@ import { timeItems, timePreferance, } from 'container/NewWidget/RightContainer/timeItems'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import getChartData from 'lib/getChartData'; import { useCallback, useMemo, useState } from 'react'; -import { useQuery } from 'react-query'; import { useSelector } from 'react-redux'; -import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults'; import { AppState } from 'store/reducers'; -import { ErrorResponse, SuccessResponse } from 'types/api'; import { Widgets } from 'types/api/dashboard/getAll'; -import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { GlobalReducer } from 'types/reducer/globalTime'; import { TimeContainer } from './styles'; @@ -44,18 +41,24 @@ function FullView({ name: getSelectedTime()?.name || '', enum: widget?.timePreferance || 'GLOBAL_TIME', }); - const response = useQuery< - SuccessResponse | ErrorResponse - >( - `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`, + + const queryKey = useMemo( () => - GetMetricQueryRange({ - selectedTime: selectedTime.enum, - graphType: widget.panelTypes, - query: widget.query, - globalSelectedInterval: globalSelectedTime, - variables: getDashboardVariables(), - }), + `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`, + [selectedTime, globalSelectedTime, widget], + ); + + const response = useGetQueryRange( + { + selectedTime: selectedTime.enum, + graphType: widget.panelTypes, + query: widget.query, + globalSelectedInterval: globalSelectedTime, + variables: getDashboardVariables(), + }, + { + queryKey, + }, ); const chartDataSet = useMemo( diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index c581f7f917..196ba14c03 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -3,6 +3,7 @@ import { ChartData } from 'chart.js'; import Spinner from 'components/Spinner'; import GridGraphComponent from 'container/GridGraphComponent'; import { UpdateDashboard } from 'container/GridGraphLayout/utils'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useNotifications } from 'hooks/useNotifications'; import usePreviousValue from 'hooks/usePreviousValue'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; @@ -20,7 +21,6 @@ import { import { Layout } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { useInView } from 'react-intersection-observer'; -import { useQuery } from 'react-query'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; @@ -28,7 +28,6 @@ import { DeleteWidget, DeleteWidgetProps, } from 'store/actions/dashboard/deleteWidget'; -import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { Widgets } from 'types/api/dashboard/getAll'; @@ -55,7 +54,7 @@ function GridCardGraph({ const { ref: graphRef, inView: isGraphVisible } = useInView({ threshold: 0, triggerOnce: true, - initialInView: true, + initialInView: false, }); const { notifications } = useNotifications(); @@ -81,33 +80,28 @@ function GridCardGraph({ const selectedData = selectedDashboard?.data; const { variables } = selectedData; - const queryResponse = useQuery( - [ - `GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget.id}`, - { + const queryResponse = useGetQueryRange( + { + selectedTime: widget?.timePreferance, + graphType: widget?.panelTypes, + query: widget?.query, + globalSelectedInterval, + variables: getDashboardVariables(), + }, + { + queryKey: [ + `GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget?.id}`, widget, maxTime, minTime, globalSelectedInterval, variables, - }, - ], - () => - GetMetricQueryRange({ - selectedTime: widget?.timePreferance, - graphType: widget.panelTypes, - query: widget.query, - globalSelectedInterval, - variables: getDashboardVariables(), - }), - { + ], keepPreviousData: true, enabled: isGraphVisible, refetchOnMount: false, onError: (error) => { - if (error instanceof Error) { - setErrorMessage(error.message); - } + setErrorMessage(error.message); }, }, ); @@ -179,7 +173,7 @@ function GridCardGraph({ { data: selectedDashboard.data, generateWidgetId: uuid, - graphType: widget.panelTypes, + graphType: widget?.panelTypes, selectedDashboard, layout, widgetData: widget, @@ -193,7 +187,7 @@ function GridCardGraph({ setTimeout(() => { history.push( - `${history.location.pathname}/new?graphType=${widget.panelTypes}&widgetId=${uuid}`, + `${history.location.pathname}/new?graphType=${widget?.panelTypes}&widgetId=${uuid}`, ); }, 1500); }); @@ -259,10 +253,10 @@ function GridCardGraph({ />
, - disabled: false, + disabled: !editWidget, label: 'Clone', }, { diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx index d299e01e7e..2235b93708 100644 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ b/frontend/src/container/GridGraphLayout/index.tsx @@ -124,8 +124,7 @@ function GridGraph(props: Props): JSX.Element { } } })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch, isAddWidget, layouts, selectedDashboard, widgets]); const { featureResponse } = useSelector( (state) => state.app, diff --git a/frontend/src/container/GridGraphLayout/utils.ts b/frontend/src/container/GridGraphLayout/utils.ts index 625557173f..6f6910b3b5 100644 --- a/frontend/src/container/GridGraphLayout/utils.ts +++ b/frontend/src/container/GridGraphLayout/utils.ts @@ -1,15 +1,10 @@ import { NotificationInstance } from 'antd/es/notification/interface'; import updateDashboardApi from 'api/dashboard/update'; -import { - initialClickHouseData, - initialQueryBuilderFormValues, - initialQueryPromQLData, -} from 'constants/queryBuilder'; +import { initialQueriesMap } from 'constants/queryBuilder'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { Layout } from 'react-grid-layout'; import store from 'store'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; -import { EQueryType } from 'types/common/dashboard'; export const UpdateDashboard = async ( { @@ -41,23 +36,7 @@ export const UpdateDashboard = async ( nullZeroValues: widgetData?.nullZeroValues || '', opacity: '', panelTypes: graphType, - query: widgetData?.query || { - queryType: EQueryType.QUERY_BUILDER, - promql: [initialQueryPromQLData], - clickhouse_sql: [initialClickHouseData], - builder: { - queryFormulas: [], - queryData: [initialQueryBuilderFormValues], - }, - }, - queryData: { - data: { - queryData: widgetData?.queryData.data.queryData || [], - }, - error: false, - errorMessage: '', - loading: false, - }, + query: widgetData?.query || initialQueriesMap.metrics, timePreferance: widgetData?.timePreferance || 'GLOBAL_TIME', title: widgetData ? copyTitle : '', }, diff --git a/frontend/src/container/Header/index.tsx b/frontend/src/container/Header/index.tsx index d3dcb52c99..b1334ade34 100644 --- a/frontend/src/container/Header/index.tsx +++ b/frontend/src/container/Header/index.tsx @@ -91,7 +91,7 @@ function HeaderContainer(): JSX.Element { const onClickSignozCloud = (): void => { window.open( - 'https://signoz.io/pricing/?utm_source=product_navbar&utm_medium=frontend', + 'https://signoz.io/oss-to-cloud/?utm_source=product_navbar&utm_medium=frontend&utm_campaign=oss_users', '_blank', ); }; diff --git a/frontend/src/container/ListAlertRules/ListAlert.tsx b/frontend/src/container/ListAlertRules/ListAlert.tsx index 41e69c718b..c1204eb79b 100644 --- a/frontend/src/container/ListAlertRules/ListAlert.tsx +++ b/frontend/src/container/ListAlertRules/ListAlert.tsx @@ -2,6 +2,7 @@ import { PlusOutlined } from '@ant-design/icons'; import { Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; +import saveAlertApi from 'api/alerts/save'; import { ResizeTable } from 'components/ResizeTable'; import TextToolTip from 'components/TextToolTip'; import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; @@ -67,7 +68,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { .catch(handleError); }, [featureResponse, handleError]); - const onEditHandler = (record: GettableAlert): void => { + const onEditHandler = (record: GettableAlert) => (): void => { featureResponse .refetch() .then(() => { @@ -84,6 +85,44 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { .catch(handleError); }; + const onCloneHandler = ( + originalAlert: GettableAlert, + ) => async (): Promise => { + const copyAlert = { + ...originalAlert, + alert: originalAlert.alert.concat(' - Copy'), + }; + const apiReq = { data: copyAlert }; + + const response = await saveAlertApi(apiReq); + + if (response.statusCode === 200) { + notificationsApi.success({ + message: 'Success', + description: 'Alert cloned successfully', + }); + + const { data: refetchData, status } = await refetch(); + if (status === 'success' && refetchData.payload) { + setData(refetchData.payload || []); + setTimeout(() => { + const clonedAlert = refetchData.payload[refetchData.payload.length - 1]; + history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${clonedAlert.id}`); + }, 2000); + } + if (status === 'error') { + notificationsApi.error({ + message: t('something_went_wrong'), + }); + } + } else { + notificationsApi.error({ + message: 'Error', + description: response.error || t('something_went_wrong'), + }); + } + }; + const columns: ColumnsType = [ { title: 'Status', @@ -107,9 +146,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { return 0; }, render: (value, record): JSX.Element => ( - onEditHandler(record)}> - {value} - + {value} ), }, { @@ -165,9 +202,12 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { <> - onEditHandler(record)} type="link"> + Edit + + Clone + diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index b737f7d0f9..3a7f406b4e 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -71,23 +71,8 @@ function ImportJSON({ setDashboardCreating(true); const dashboardData = JSON.parse(editorValue) as DashboardData; - // removing the queryData - const parsedWidgets: DashboardData = { - ...dashboardData, - widgets: dashboardData.widgets?.map((e) => ({ - ...e, - queryData: { - ...e.queryData, - data: e.queryData.data, - error: false, - errorMessage: '', - loading: false, - }, - })), - }; - const response = await createDashboard({ - ...parsedWidgets, + ...dashboardData, uploadedGrafana, }); diff --git a/frontend/src/container/LogControls/config.ts b/frontend/src/container/LogControls/config.ts deleted file mode 100644 index 7d3b0f71ee..0000000000 --- a/frontend/src/container/LogControls/config.ts +++ /dev/null @@ -1 +0,0 @@ -export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200]; diff --git a/frontend/src/container/LogControls/index.tsx b/frontend/src/container/LogControls/index.tsx index 68c4c5a08b..d5f28ba45f 100644 --- a/frontend/src/container/LogControls/index.tsx +++ b/frontend/src/container/LogControls/index.tsx @@ -1,16 +1,11 @@ -import { - CloudDownloadOutlined, - FastBackwardOutlined, - LeftOutlined, - RightOutlined, -} from '@ant-design/icons'; -import { Button, Divider, Dropdown, MenuProps, Select } from 'antd'; +import { CloudDownloadOutlined, FastBackwardOutlined } from '@ant-design/icons'; +import { Button, Divider, Dropdown, MenuProps } from 'antd'; import { Excel } from 'antd-table-saveas-excel'; +import Controls from 'container/Controls'; import { getGlobalTime } from 'container/LogsSearchFilter/utils'; import { getMinMax } from 'container/TopNav/AutoRefresh/config'; import dayjs from 'dayjs'; import { FlatLogData } from 'lib/logs/flatLogData'; -import { defaultSelectStyle } from 'pages/Logs/config'; import * as Papa from 'papaparse'; import { memo, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -26,7 +21,6 @@ import { import { GlobalReducer } from 'types/reducer/globalTime'; import { ILogsReducer } from 'types/reducer/logs'; -import { ITEMS_PER_PAGE_OPTIONS } from './config'; import { Container, DownloadLogButton } from './styles'; function LogControls(): JSX.Element | null { @@ -149,15 +143,6 @@ function LogControls(): JSX.Element | null { const isLoading = isLogsLoading || isLoadingAggregate; - const isNextAndPreviousDisabled = useMemo( - () => - isLoading || - logLinesPerPage === 0 || - logs.length === 0 || - logs.length < logLinesPerPage, - [isLoading, logLinesPerPage, logs.length], - ); - if (liveTail !== 'STOPPED') { return null; } @@ -179,37 +164,14 @@ function LogControls(): JSX.Element | null { Go to latest - - - + ); } diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 2959be7dcc..7d4db12acc 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -32,7 +32,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null { const dispatch = useDispatch>(); - const flattenLogData: Record | null = useMemo( + const flattenLogData: Record | null = useMemo( () => (logData ? flattenObject(logData) : null), [logData], ); diff --git a/frontend/src/container/LogsExplorerChart/LogsExplorerChart.styled.ts b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.styled.ts new file mode 100644 index 0000000000..6fbe2d2e23 --- /dev/null +++ b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.styled.ts @@ -0,0 +1,11 @@ +import { Card } from 'antd'; +import styled from 'styled-components'; + +export const CardStyled = styled(Card)` + position: relative; + margin: 0.5rem 0 3.1rem 0; + .ant-card-body { + height: 20vh; + min-height: 200px; + } +`; diff --git a/frontend/src/container/LogsExplorerChart/LogsExplorerChart.tsx b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.tsx new file mode 100644 index 0000000000..6c2307efc2 --- /dev/null +++ b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.tsx @@ -0,0 +1,66 @@ +import Graph from 'components/Graph'; +import Spinner from 'components/Spinner'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { getExplorerChartData } from 'lib/explorer/getExplorerChartData'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { CardStyled } from './LogsExplorerChart.styled'; + +export function LogsExplorerChart(): JSX.Element { + const { stagedQuery } = useQueryBuilder(); + + const { selectedTime } = useSelector( + (state) => state.globalTime, + ); + + const panelTypeParam = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); + + const { data, isFetching } = useGetQueryRange( + { + query: stagedQuery || initialQueriesMap.metrics, + graphType: panelTypeParam, + globalSelectedInterval: selectedTime, + selectedTime: 'GLOBAL_TIME', + }, + { + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + selectedTime, + stagedQuery, + panelTypeParam, + ], + enabled: !!stagedQuery, + }, + ); + + const graphData = useMemo(() => { + if (data?.payload.data && data.payload.data.result.length > 0) { + return getExplorerChartData([data.payload.data.result[0]]); + } + + return getExplorerChartData([]); + }, [data]); + + return ( + + {isFetching ? ( + + ) : ( + + )} + + ); +} diff --git a/frontend/src/container/LogsExplorerChart/index.ts b/frontend/src/container/LogsExplorerChart/index.ts new file mode 100644 index 0000000000..48d9469dba --- /dev/null +++ b/frontend/src/container/LogsExplorerChart/index.ts @@ -0,0 +1 @@ +export { LogsExplorerChart } from './LogsExplorerChart'; diff --git a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts new file mode 100644 index 0000000000..4fd3046e3b --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts @@ -0,0 +1,9 @@ +import { Tabs } from 'antd'; +import { themeColors } from 'constants/theme'; +import styled from 'styled-components'; + +export const TabsStyled = styled(Tabs)` + & .ant-tabs-nav { + background-color: ${themeColors.lightBlack}; + } +`; diff --git a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.tsx b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.tsx new file mode 100644 index 0000000000..303db79f01 --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.tsx @@ -0,0 +1,75 @@ +import { TabsProps } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import { TabsStyled } from './LogsExplorerViews.styled'; + +export function LogsExplorerViews(): JSX.Element { + const location = useLocation(); + const urlQuery = useUrlQuery(); + const history = useHistory(); + const { currentQuery } = useQueryBuilder(); + + const panelTypeParams = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); + + const isMultipleQueries = useMemo( + () => + currentQuery.builder.queryData.length > 1 || + currentQuery.builder.queryFormulas.length > 0, + [currentQuery], + ); + + const tabsItems: TabsProps['items'] = useMemo( + () => [ + { + label: 'List View', + key: PANEL_TYPES.LIST, + disabled: isMultipleQueries, + }, + { label: 'TimeSeries', key: PANEL_TYPES.TIME_SERIES }, + { label: 'Table', key: PANEL_TYPES.TABLE }, + ], + [isMultipleQueries], + ); + + const handleChangeView = useCallback( + (panelType: string) => { + urlQuery.set(PANEL_TYPES_QUERY, JSON.stringify(panelType) as GRAPH_TYPES); + const path = `${location.pathname}?${urlQuery}`; + + history.push(path); + }, + [history, location, urlQuery], + ); + + const currentTabKey = useMemo( + () => + Object.values(PANEL_TYPES).includes(panelTypeParams) + ? panelTypeParams + : PANEL_TYPES.LIST, + [panelTypeParams], + ); + + useEffect(() => { + if (panelTypeParams === 'list' && isMultipleQueries) { + handleChangeView(PANEL_TYPES.TIME_SERIES); + } + }, [panelTypeParams, isMultipleQueries, handleChangeView]); + + return ( +
+ +
+ ); +} diff --git a/frontend/src/container/LogsExplorerViews/index.ts b/frontend/src/container/LogsExplorerViews/index.ts new file mode 100644 index 0000000000..4a29ed0988 --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/index.ts @@ -0,0 +1 @@ +export { LogsExplorerViews } from './LogsExplorerViews'; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx index 4b01ba02e2..1a16349fb9 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx @@ -6,7 +6,7 @@ import { QueryOperatorsMultiVal, QueryOperatorsSingleVal, } from 'lib/logql/tokens'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { ILogsReducer } from 'types/reducer/logs'; @@ -56,6 +56,8 @@ function QueryField({ onUpdate, onDelete, }: QueryFieldProps): JSX.Element | null { + const [isDropDownOpen, setIsDropDownOpen] = useState(false); + const { fields: { selected }, } = useSelector((store) => store.logs); @@ -136,9 +138,12 @@ function QueryField({ ({ opacity: '0', panelTypes: PANEL_TYPES.TIME_SERIES, query, - queryData: { - data: { queryData: [] }, - error: false, - errorMessage: '', - loading: false, - }, timePreferance: 'GLOBAL_TIME', title: '', }); diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts index 57d4829ea3..b678c833a2 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts @@ -1,6 +1,6 @@ import { initialFormulaBuilderFormValues, - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, } from 'constants/queryBuilder'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; @@ -18,7 +18,7 @@ export const getQueryBuilderQueries = ({ queryFormulas: [], queryData: [ { - ...initialQueryBuilderFormValues, + ...initialQueryBuilderFormValuesMap.metrics, aggregateOperator: MetricAggregateOperator.SUM_RATE, disabled: false, groupBy, @@ -53,7 +53,7 @@ export const getQueryBuilderQuerieswithFormula = ({ ], queryData: [ { - ...initialQueryBuilderFormValues, + ...initialQueryBuilderFormValuesMap.metrics, aggregateOperator: MetricAggregateOperator.SUM_RATE, disabled, groupBy, @@ -66,7 +66,7 @@ export const getQueryBuilderQuerieswithFormula = ({ }, }, { - ...initialQueryBuilderFormValues, + ...initialQueryBuilderFormValuesMap.metrics, aggregateOperator: MetricAggregateOperator.SUM_RATE, disabled, groupBy, diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index ca4c374125..74df7c2dc3 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -14,6 +14,7 @@ import { useParams } from 'react-router-dom'; import { Widgets } from 'types/api/dashboard/getAll'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; +import { v4 as uuid } from 'uuid'; import { Card, GraphContainer, GraphTitle, Row } from '../styles'; import { Button } from './styles'; @@ -56,6 +57,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -69,6 +71,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index 1a4a511653..37e206b933 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -15,6 +15,7 @@ import { useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Widgets } from 'types/api/dashboard/getAll'; import { EQueryType } from 'types/common/dashboard'; +import { v4 as uuid } from 'uuid'; import { Card, GraphContainer, GraphTitle, Row } from '../styles'; import { legend } from './constant'; @@ -48,6 +49,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -67,6 +69,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -82,6 +85,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -97,6 +101,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 3fb81df2e0..75d2109e8e 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -21,6 +21,7 @@ import { AppState } from 'store/reducers'; import { Widgets } from 'types/api/dashboard/getAll'; import { EQueryType } from 'types/common/dashboard'; import MetricReducer from 'types/reducer/metrics'; +import { v4 as uuid } from 'uuid'; import { errorPercentage, @@ -91,6 +92,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { topLevelOperations, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems], ); @@ -106,6 +108,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { topLevelOperations, }), clickhouse_sql: [], + id: uuid(), }), [servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder], ); diff --git a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx index 540bc0d546..739ef8af0e 100644 --- a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx +++ b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx @@ -11,6 +11,8 @@ import { useParams } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { GlobalReducer } from 'types/reducer/globalTime'; +import { getErrorRate } from './utils'; + function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { const { minTime, maxTime } = useSelector( (state) => state.globalTime, @@ -89,10 +91,10 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { dataIndex: 'errorCount', key: 'errorCount', width: 50, - sorter: (a: TopOperationList, b: TopOperationList): number => - a.errorCount - b.errorCount, - render: (value: number, record: TopOperationList): string => - `${((value / record.numCalls) * 100).toFixed(2)} %`, + sorter: (first: TopOperationList, second: TopOperationList): number => + getErrorRate(first) - getErrorRate(second), + render: (_, record: TopOperationList): string => + `${getErrorRate(record).toFixed(2)} %`, }, ]; diff --git a/frontend/src/container/MetricsApplication/utils.ts b/frontend/src/container/MetricsApplication/utils.ts new file mode 100644 index 0000000000..a27242718c --- /dev/null +++ b/frontend/src/container/MetricsApplication/utils.ts @@ -0,0 +1,4 @@ +import { TopOperationList } from './TopOperationsTable'; + +export const getErrorRate = (list: TopOperationList): number => + (list.errorCount / list.numCalls) * 100; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index af1e14feaa..b20c4388f8 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { initialQueryWithType } from 'constants/queryBuilder'; +import { initialQueriesMap } from 'constants/queryBuilder'; import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; @@ -47,7 +47,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { history.push( `${history.location.pathname}/new?graphType=${name}&widgetId=${ emptyLayout.i - }&${COMPOSITE_QUERY}=${JSON.stringify(initialQueryWithType)}`, + }&${COMPOSITE_QUERY}=${JSON.stringify(initialQueriesMap.metrics)}`, ); } catch (error) { notifications.error({ diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx index 67d961f5c9..d318e684f8 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx @@ -10,6 +10,7 @@ import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboa import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import AppReducer from 'types/reducer/app'; import DashboardReducer from 'types/reducer/dashboards'; import VariableItem from './VariableItem'; @@ -29,6 +30,8 @@ function DashboardVariableSelection({ const [lastUpdatedVar, setLastUpdatedVar] = useState(''); const { notifications } = useNotifications(); + const { role } = useSelector((state) => state.app); + const onVarChanged = (name: string): void => { setLastUpdatedVar(name); setUpdate(!update); @@ -36,19 +39,15 @@ function DashboardVariableSelection({ const onValueUpdate = ( name: string, - value: - | string - | string[] - | number - | number[] - | boolean - | boolean[] - | null - | undefined, + value: IDashboardVariable['selectedValue'], ): void => { const updatedVariablesData = { ...variables }; updatedVariablesData[name].selectedValue = value; - updateDashboardVariables(updatedVariablesData, notifications); + + if (role !== 'VIEWER') { + updateDashboardVariables(updatedVariablesData, notifications); + } + onVarChanged(name); }; const onAllSelectedUpdate = ( @@ -57,7 +56,10 @@ function DashboardVariableSelection({ ): void => { const updatedVariablesData = { ...variables }; updatedVariablesData[name].allSelected = value; - updateDashboardVariables(updatedVariablesData, notifications); + + if (role !== 'VIEWER') { + updateDashboardVariables(updatedVariablesData, notifications); + } onVarChanged(name); }; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/ShareModal.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/ShareModal.tsx index 51d83b23a3..d7ccd451e4 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/ShareModal.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/ShareModal.tsx @@ -6,28 +6,14 @@ import { useTranslation } from 'react-i18next'; import { useCopyToClipboard } from 'react-use'; import { DashboardData } from 'types/api/dashboard/getAll'; -import { cleardQueryData, downloadObjectAsJson } from './util'; +import { downloadObjectAsJson } from './util'; function ShareModal({ isJSONModalVisible, onToggleHandler, selectedData, }: ShareModalProps): JSX.Element { - const getParsedValue = (): string => { - const updatedData: DashboardData = { - ...selectedData, - widgets: selectedData.widgets?.map((widget) => ({ - ...widget, - queryData: { - ...widget.queryData, - loading: false, - error: false, - errorMessage: '', - }, - })), - }; - return JSON.stringify(updatedData, null, 2); - }; + const getParsedValue = (): string => JSON.stringify(selectedData, null, 2); const [jsonValue, setJSONValue] = useState(getParsedValue()); const [isViewJSON, setIsViewJSON] = useState(false); @@ -53,7 +39,6 @@ function ShareModal({ } }, [state.error, state.value, t, notifications]); - const selectedDataCleaned = cleardQueryData(selectedData); const GetFooterComponent = useMemo(() => { if (!isViewJSON) { return ( @@ -69,7 +54,7 @@ function ShareModal({ ); - }, [isViewJSON, jsonValue, selectedData, selectedDataCleaned, setCopy, t]); + }, [isViewJSON, jsonValue, selectedData, setCopy, t]); return ( ({ - ...widget, - queryData: { - ...widget.queryData, - data: { - queryData: [], - }, - }, - })), - }; -} diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index 1d8cbd763b..d3e52120b3 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -1,11 +1,13 @@ import { Button, Tabs, Typography } from 'antd'; import TextToolTip from 'components/TextToolTip'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { WidgetGraphProps } from 'container/NewWidget/types'; import { QueryBuilder } from 'container/QueryBuilder'; +import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import useUrlQuery from 'hooks/useUrlQuery'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback } from 'react'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; @@ -18,21 +20,31 @@ import AppActions from 'types/actions'; import { Widgets } from 'types/api/dashboard/getAll'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; +import AppReducer from 'types/reducer/app'; import DashboardReducer from 'types/reducer/dashboards'; import ClickHouseQueryContainer from './QueryBuilder/clickHouse'; import PromQLQueryContainer from './QueryBuilder/promQL'; -function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { +function QuerySection({ + updateQuery, + selectedGraph, + selectedTime, +}: QueryProps): JSX.Element { const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); const urlQuery = useUrlQuery(); + const { featureResponse } = useSelector( + (state) => state.app, + ); - const [isInit, setIsInit] = useState(false); + const { dashboards } = useSelector( + (state) => state.dashboards, + ); - const { dashboards, isLoadingQueryResult } = useSelector< - AppState, - DashboardReducer - >((state) => state.dashboards); + const getWidgetQueryRange = useGetWidgetQueryRange({ + graphType: selectedGraph, + selectedTime: selectedTime.enum, + }); const [selectedDashboards] = dashboards; const { widgets } = selectedDashboards.data; @@ -46,23 +58,11 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { const { query } = selectedWidget; - const { compositeQuery } = useShareBuilderUrl({ defaultValue: query }); - - useEffect(() => { - if (!isInit && compositeQuery) { - setIsInit(true); - updateQuery({ - updatedQuery: compositeQuery, - widgetId: urlQuery.get('widgetId') || '', - yAxisUnit: selectedWidget.yAxisUnit, - }); - } - }, [isInit, compositeQuery, selectedWidget, urlQuery, updateQuery]); + useShareBuilderUrl({ defaultValue: query }); const handleStageQuery = useCallback( (updatedQuery: Query): void => { updateQuery({ - updatedQuery, widgetId: urlQuery.get('widgetId') || '', yAxisUnit: selectedWidget.yAxisUnit, }); @@ -76,7 +76,9 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { const handleQueryCategoryChange = (qCategory: string): void => { const currentQueryType = qCategory as EQueryType; - handleStageQuery({ ...currentQuery, queryType: currentQueryType }); + featureResponse.refetch().then(() => { + handleStageQuery({ ...currentQuery, queryType: currentQueryType }); + }); }; const handleRunQuery = (): void => { @@ -115,7 +117,7 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { )} @@ -231,7 +227,11 @@ function NewWidget({ - + @@ -270,34 +270,24 @@ function NewWidget({ width={600} > - Your graph built with{' '} - query will be - saved. Press OK to confirm. + Your graph built with {' '} + query will be saved. Press OK to confirm. ); } -export interface NewWidgetProps { - selectedGraph: GRAPH_TYPES; - yAxisUnit: Widgets['yAxisUnit']; -} - interface DispatchProps { saveSettingOfPanel: ( props: SaveDashboardProps, ) => (dispatch: Dispatch) => void; - getQueryResults: ( - props: GetQueryResultsProps, - ) => (dispatch: Dispatch) => void; } const mapDispatchToProps = ( dispatch: ThunkDispatch, ): DispatchProps => ({ saveSettingOfPanel: bindActionCreators(SaveDashboard, dispatch), - getQueryResults: bindActionCreators(GetQueryResults, dispatch), }); type Props = DispatchProps & NewWidgetProps; diff --git a/frontend/src/container/NewWidget/types.ts b/frontend/src/container/NewWidget/types.ts new file mode 100644 index 0000000000..ca6a121382 --- /dev/null +++ b/frontend/src/container/NewWidget/types.ts @@ -0,0 +1,13 @@ +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { Widgets } from 'types/api/dashboard/getAll'; + +import { timePreferance } from './RightContainer/timeItems'; + +export interface NewWidgetProps { + selectedGraph: GRAPH_TYPES; + yAxisUnit: Widgets['yAxisUnit']; +} + +export interface WidgetGraphProps extends NewWidgetProps { + selectedTime: timePreferance; +} diff --git a/frontend/src/container/OptionsMenu/AddColumnField/index.tsx b/frontend/src/container/OptionsMenu/AddColumnField/index.tsx new file mode 100644 index 0000000000..2e789a1024 --- /dev/null +++ b/frontend/src/container/OptionsMenu/AddColumnField/index.tsx @@ -0,0 +1,43 @@ +import { SearchOutlined } from '@ant-design/icons'; +import { Input } from 'antd'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useTranslation } from 'react-i18next'; + +import { OptionsMenuConfig } from '..'; +import { FieldTitle } from '../styles'; +import { AddColumnSelect, AddColumnWrapper, SearchIconWrapper } from './styles'; + +function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null { + const { t } = useTranslation(['trace']); + const isDarkMode = useIsDarkMode(); + + if (!config) return null; + + return ( + + {t('options_menu.addColumn')} + + + + + + + + + ); +} + +interface AddColumnFieldProps { + config: OptionsMenuConfig['addColumn']; +} + +export default AddColumnField; diff --git a/frontend/src/container/OptionsMenu/AddColumnField/styles.ts b/frontend/src/container/OptionsMenu/AddColumnField/styles.ts new file mode 100644 index 0000000000..72df5467af --- /dev/null +++ b/frontend/src/container/OptionsMenu/AddColumnField/styles.ts @@ -0,0 +1,28 @@ +import { Card, Select, SelectProps, Space } from 'antd'; +import { themeColors } from 'constants/theme'; +import { FunctionComponent } from 'react'; +import styled from 'styled-components'; + +export const SearchIconWrapper = styled(Card)<{ $isDarkMode: boolean }>` + width: 15%; + border-color: ${({ $isDarkMode }): string => + $isDarkMode ? themeColors.borderDarkGrey : themeColors.borderLightGrey}; + + .ant-card-body { + display: flex; + justify-content: center; + align-items: center; + padding: 0.25rem; + font-size: 0.875rem; + } +`; + +export const AddColumnSelect: FunctionComponent = styled( + Select, +)` + width: 85%; +`; + +export const AddColumnWrapper = styled(Space)` + width: 100%; +`; diff --git a/frontend/src/container/OptionsMenu/FormatField/index.tsx b/frontend/src/container/OptionsMenu/FormatField/index.tsx new file mode 100644 index 0000000000..09e6743ade --- /dev/null +++ b/frontend/src/container/OptionsMenu/FormatField/index.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from 'react-i18next'; + +import { OptionsMenuConfig } from '..'; +import { FieldTitle } from '../styles'; +import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles'; + +function FormatField({ config }: FormatFieldProps): JSX.Element | null { + const { t } = useTranslation(['trace']); + + if (!config) return null; + + return ( + + {t('options_menu.format')} + + {t('options_menu.row')} + {t('options_menu.default')} + {t('options_menu.column')} + + + ); +} + +interface FormatFieldProps { + config: OptionsMenuConfig['format']; +} + +export default FormatField; diff --git a/frontend/src/container/OptionsMenu/FormatField/styles.ts b/frontend/src/container/OptionsMenu/FormatField/styles.ts new file mode 100644 index 0000000000..d98270a80f --- /dev/null +++ b/frontend/src/container/OptionsMenu/FormatField/styles.ts @@ -0,0 +1,17 @@ +import { Radio, Space } from 'antd'; +import styled from 'styled-components'; + +export const FormatFieldWrapper = styled(Space)` + width: 100%; + margin-bottom: 1.125rem; +`; + +export const RadioGroup = styled(Radio.Group)` + display: flex; + text-align: center; +`; + +export const RadioButton = styled(Radio.Button)` + font-size: 0.75rem; + flex: 1; +`; diff --git a/frontend/src/container/OptionsMenu/MaxLinesField/index.tsx b/frontend/src/container/OptionsMenu/MaxLinesField/index.tsx new file mode 100644 index 0000000000..7c10423f53 --- /dev/null +++ b/frontend/src/container/OptionsMenu/MaxLinesField/index.tsx @@ -0,0 +1,29 @@ +import { useTranslation } from 'react-i18next'; + +import { OptionsMenuConfig } from '..'; +import { FieldTitle } from '../styles'; +import { MaxLinesFieldWrapper, MaxLinesInput } from './styles'; + +function MaxLinesField({ config }: MaxLinesFieldProps): JSX.Element | null { + const { t } = useTranslation(['trace']); + + if (!config) return null; + + return ( + + {t('options_menu.maxLines')} + + + ); +} + +interface MaxLinesFieldProps { + config: OptionsMenuConfig['maxLines']; +} + +export default MaxLinesField; diff --git a/frontend/src/container/OptionsMenu/MaxLinesField/styles.ts b/frontend/src/container/OptionsMenu/MaxLinesField/styles.ts new file mode 100644 index 0000000000..6c177ff5a4 --- /dev/null +++ b/frontend/src/container/OptionsMenu/MaxLinesField/styles.ts @@ -0,0 +1,12 @@ +import { InputNumber } from 'antd'; +import styled from 'styled-components'; + +export const MaxLinesFieldWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +export const MaxLinesInput = styled(InputNumber)` + max-width: 46px; +`; diff --git a/frontend/src/container/OptionsMenu/index.tsx b/frontend/src/container/OptionsMenu/index.tsx new file mode 100644 index 0000000000..96059ef06c --- /dev/null +++ b/frontend/src/container/OptionsMenu/index.tsx @@ -0,0 +1,57 @@ +import { SettingFilled, SettingOutlined } from '@ant-design/icons'; +import { + InputNumberProps, + Popover, + RadioProps, + SelectProps, + Space, +} from 'antd'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import AddColumnField from './AddColumnField'; +import FormatField from './FormatField'; +import MaxLinesField from './MaxLinesField'; +import { OptionsContainer, OptionsContentWrapper } from './styles'; + +function OptionsMenu({ config }: OptionsMenuProps): JSX.Element { + const { t } = useTranslation(['trace']); + const isDarkMode = useIsDarkMode(); + + const OptionsContent = useMemo( + () => ( + + {config?.format && } + {config?.maxLines && } + {config?.addColumn && } + + ), + [config], + ); + + const SettingIcon = isDarkMode ? SettingOutlined : SettingFilled; + + return ( + + + + {t('options_menu.options')} + + + + + ); +} + +export type OptionsMenuConfig = { + format?: Pick; + maxLines?: Pick; + addColumn?: Pick; +}; + +interface OptionsMenuProps { + config: OptionsMenuConfig; +} + +export default OptionsMenu; diff --git a/frontend/src/container/OptionsMenu/styles.ts b/frontend/src/container/OptionsMenu/styles.ts new file mode 100644 index 0000000000..08530660b7 --- /dev/null +++ b/frontend/src/container/OptionsMenu/styles.ts @@ -0,0 +1,19 @@ +import { Card, Space, Typography } from 'antd'; +import styled from 'styled-components'; + +export const OptionsContainer = styled(Card)` + .ant-card-body { + display: flex; + padding: 0.25rem 0.938rem; + cursor: pointer; + } +`; + +export const OptionsContentWrapper = styled(Space)` + min-width: 11rem; + padding: 0.25rem 0.5rem; +`; + +export const FieldTitle = styled(Typography.Text)` + font-size: 0.75rem; +`; diff --git a/frontend/src/container/OrganizationSettings/PendingInvitesContainer/index.tsx b/frontend/src/container/OrganizationSettings/PendingInvitesContainer/index.tsx index e5d6eb06ac..76c25c09b3 100644 --- a/frontend/src/container/OrganizationSettings/PendingInvitesContainer/index.tsx +++ b/frontend/src/container/OrganizationSettings/PendingInvitesContainer/index.tsx @@ -55,12 +55,18 @@ function PendingInvitesContainer(): JSX.Element { queryKey: ['getPendingInvites', user?.accessJwt], }); - const toggleModal = (value: boolean): void => { - setIsInviteTeamMemberModalOpen(value); - }; - const [dataSource, setDataSource] = useState([]); + const toggleModal = useCallback( + (value: boolean): void => { + setIsInviteTeamMemberModalOpen(value); + if (!value) { + form.resetFields(); + } + }, + [form], + ); + const { hash } = useLocation(); const getParsedInviteData = useCallback( @@ -79,7 +85,7 @@ function PendingInvitesContainer(): JSX.Element { if (hash === INVITE_MEMBERS_HASH) { toggleModal(true); } - }, [hash]); + }, [hash, toggleModal]); useEffect(() => { if ( @@ -225,7 +231,13 @@ function PendingInvitesContainer(): JSX.Element { }); } }, - [getParsedInviteData, getPendingInvitesResponse, notifications, t], + [ + getParsedInviteData, + getPendingInvitesResponse, + notifications, + t, + toggleModal, + ], ); return ( diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts index 41ea0b5b3f..ab61c94f22 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts +++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts @@ -1,4 +1,5 @@ import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; +import { ReactNode } from 'react'; import { DataSource } from 'types/common/queryBuilder'; export type QueryBuilderConfig = @@ -11,4 +12,5 @@ export type QueryBuilderConfig = export type QueryBuilderProps = { config?: QueryBuilderConfig; panelType: ITEMS; + actions?: ReactNode; }; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.styled.ts b/frontend/src/container/QueryBuilder/QueryBuilder.styled.ts new file mode 100644 index 0000000000..20dc483e01 --- /dev/null +++ b/frontend/src/container/QueryBuilder/QueryBuilder.styled.ts @@ -0,0 +1,6 @@ +import { Col } from 'antd'; +import styled from 'styled-components'; + +export const ActionsWrapperStyled = styled(Col)` + padding-right: 1rem; +`; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/QueryBuilder/QueryBuilder.tsx index e0900cc947..dae8717920 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/QueryBuilder/QueryBuilder.tsx @@ -11,15 +11,16 @@ import { Formula, Query } from './components'; // ** Types import { QueryBuilderProps } from './QueryBuilder.interfaces'; // ** Styles +import { ActionsWrapperStyled } from './QueryBuilder.styled'; export const QueryBuilder = memo(function QueryBuilder({ config, panelType, + actions, }: QueryBuilderProps): JSX.Element { const { currentQuery, setupInitialDataSource, - resetQueryBuilderInfo, addNewBuilderQuery, addNewFormula, handleSetPanelType, @@ -35,13 +36,6 @@ export const QueryBuilder = memo(function QueryBuilder({ handleSetPanelType(panelType); }, [handleSetPanelType, panelType]); - useEffect( - () => (): void => { - resetQueryBuilderInfo(); - }, - [resetQueryBuilderInfo], - ); - const isDisabledQueryButton = useMemo( () => currentQuery.builder.queryData.length >= MAX_QUERIES, [currentQuery], @@ -60,7 +54,7 @@ export const QueryBuilder = memo(function QueryBuilder({ ); return ( - + {currentQuery.builder.queryData.map((query, index) => ( @@ -81,28 +75,31 @@ export const QueryBuilder = memo(function QueryBuilder({ - - - - - - - - + + + + + + + + + {actions} + + ); }); diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx index 79aa1b0715..d50a754e4c 100644 --- a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx +++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx @@ -1,4 +1,4 @@ -import { Col, Row } from 'antd'; +import { Col, Row, Typography } from 'antd'; import { Fragment, memo, ReactNode, useState } from 'react'; // ** Types @@ -46,7 +46,9 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({ {isOpenedFilters ? : } - {!isOpenedFilters && Add conditions for {filtersTexts}} + {!isOpenedFilters && ( + Add conditions for {filtersTexts} + )} {isOpenedFilters && {children}} diff --git a/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx index 8fd4421f2a..9d1c17514d 100644 --- a/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx +++ b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx @@ -1,3 +1,4 @@ +import { Typography } from 'antd'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { memo } from 'react'; @@ -11,5 +12,9 @@ export const FilterLabel = memo(function FilterLabel({ }: FilterLabelProps): JSX.Element { const isDarkMode = useIsDarkMode(); - return {label}; + return ( + + {label} + + ); }); diff --git a/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx b/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx index f22d88a66a..5567947f44 100644 --- a/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx +++ b/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx @@ -3,19 +3,17 @@ import userEvent from '@testing-library/user-event'; // Constants import { HAVING_OPERATORS, - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, } from 'constants/queryBuilder'; import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData'; // ** Types import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; -import { DataSource } from 'types/common/queryBuilder'; // ** Components import { HavingFilter } from '../HavingFilter'; const valueWithAttributeAndOperator: IBuilderQuery = { - ...initialQueryBuilderFormValues, - dataSource: DataSource.LOGS, + ...initialQueryBuilderFormValuesMap.logs, aggregateOperator: 'SUM', aggregateAttribute: { isColumn: false, @@ -29,7 +27,10 @@ describe('Having filter behaviour', () => { test('Having filter render is rendered', () => { const mockFn = jest.fn(); const { unmount } = render( - , + , ); const selectId = 'havingSelect'; @@ -44,7 +45,10 @@ describe('Having filter behaviour', () => { test('Having render is disabled initially', () => { const mockFn = jest.fn(); const { unmount } = render( - , + , ); const input = screen.getByRole('combobox'); diff --git a/frontend/src/container/SideNav/config.ts b/frontend/src/container/SideNav/config.ts index f14da541c7..0246bc0e8e 100644 --- a/frontend/src/container/SideNav/config.ts +++ b/frontend/src/container/SideNav/config.ts @@ -30,6 +30,7 @@ export const routeConfig: Record = { [ROUTES.SETTINGS]: [QueryParams.resourceAttributes], [ROUTES.SIGN_UP]: [QueryParams.resourceAttributes], [ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes], + [ROUTES.TRACES_EXPLORER]: [QueryParams.resourceAttributes], [ROUTES.TRACE]: [QueryParams.resourceAttributes], [ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes], [ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes], diff --git a/frontend/src/container/SideNav/helper.ts b/frontend/src/container/SideNav/helper.ts index 6160702336..988f3196b6 100644 --- a/frontend/src/container/SideNav/helper.ts +++ b/frontend/src/container/SideNav/helper.ts @@ -1,8 +1,8 @@ export const getQueryString = ( - avialableParams: string[], + availableParams: string[], params: URLSearchParams, ): string[] => - avialableParams.map((param) => { + availableParams.map((param) => { if (params.has(param)) { return `${param}=${params.get(param)}`; } diff --git a/frontend/src/container/SideNav/index.tsx b/frontend/src/container/SideNav/index.tsx index a65a590589..2fe53d4d44 100644 --- a/frontend/src/container/SideNav/index.tsx +++ b/frontend/src/container/SideNav/index.tsx @@ -1,5 +1,5 @@ import { CheckCircleTwoTone, WarningOutlined } from '@ant-design/icons'; -import { Menu, Space, Typography } from 'antd'; +import { Menu, MenuProps } from 'antd'; import getLocalStorageKey from 'api/browser/localstorage/get'; import { IS_SIDEBAR_COLLAPSED } from 'constants/app'; import ROUTES from 'constants/routes'; @@ -27,7 +27,6 @@ import { Sider, SlackButton, SlackMenuItemContainer, - Tags, VersionContainer, } from './styles'; @@ -42,6 +41,7 @@ function SideNav(): JSX.Element { >((state) => state.app); const { pathname, search } = useLocation(); + const { t } = useTranslation(''); const onCollapse = useCallback(() => { @@ -55,9 +55,9 @@ function SideNav(): JSX.Element { const onClickHandler = useCallback( (to: string) => { const params = new URLSearchParams(search); - const avialableParams = routeConfig[to]; + const availableParams = routeConfig[to]; - const queryString = getQueryString(avialableParams, params); + const queryString = getQueryString(availableParams || [], params); if (pathname !== to) { history.push(`${to}?${queryString.join('&')}`); @@ -66,6 +66,10 @@ function SideNav(): JSX.Element { [pathname, search], ); + const onClickMenuHandler: MenuProps['onClick'] = (e) => { + onClickHandler(e.key); + }; + const onClickSlackHandler = (): void => { window.open('https://signoz.io/slack', '_blank'); }; @@ -104,30 +108,17 @@ function SideNav(): JSX.Element { }, ]; - const currentMenu = useMemo( - () => menus.find((menu) => pathname.startsWith(menu.to)), - [pathname], - ); + const currentMenu = useMemo(() => { + const routeKeys = Object.keys(ROUTES) as (keyof typeof ROUTES)[]; + const currentRouteKey = routeKeys.find((key) => { + const route = ROUTES[key]; + return pathname === route; + }); - const items = [ - ...menus.map(({ to, Icon, name, tags, children }) => ({ - key: to, - icon: , - onClick: (): void => onClickHandler(to), - label: ( - -
{name}
- {tags && - tags.map((e) => ( - - {e} - - ))} -
- ), - children, - })), - ]; + if (!currentRouteKey) return null; + + return ROUTES[currentRouteKey]; + }, [pathname]); const sidebarItems = (props: SidebarItem, index: number): SidebarItem => ({ key: `${index}`, @@ -141,10 +132,11 @@ function SideNav(): JSX.Element { {sidebar.map((props, index) => ( ['items'][number][]; -} - -export default menus; diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx new file mode 100644 index 0000000000..3da53f194c --- /dev/null +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -0,0 +1,113 @@ +import { + AlertOutlined, + AlignLeftOutlined, + ApiOutlined, + BarChartOutlined, + BugOutlined, + DashboardFilled, + DeploymentUnitOutlined, + LineChartOutlined, + MenuOutlined, + SettingOutlined, +} from '@ant-design/icons'; +import { MenuProps, Space, Typography } from 'antd'; +import ROUTES from 'constants/routes'; + +import { Tags } from './styles'; + +type MenuItem = Required['items'][number]; + +export const createLabelWithTags = ( + label: string, + tags: string[], +): JSX.Element => ( + +
{label}
+ {tags.map((tag) => ( + + {tag} + + ))} +
+); + +const menus: SidebarMenu[] = [ + { + key: ROUTES.APPLICATION, + label: 'Services', + icon: , + }, + { + key: ROUTES.TRACE, + label: 'Traces', + icon: , + // children: [ + // { + // key: ROUTES.TRACE, + // label: 'Traces', + // }, + // TODO: uncomment when will be ready explorer + // { + // key: ROUTES.TRACES_EXPLORER, + // label: "Explorer", + // }, + // ], + }, + { + key: ROUTES.LOGS, + label: 'Logs', + icon: , + // children: [ + // { + // key: ROUTES.LOGS, + // label: 'Search', + // }, + // TODO: uncomment when will be ready explorer + // { + // key: ROUTES.LOGS_EXPLORER, + // label: 'Views', + // }, + // ], + }, + { + key: ROUTES.ALL_DASHBOARD, + label: 'Dashboards', + icon: , + }, + { + key: ROUTES.LIST_ALL_ALERT, + label: 'Alerts', + icon: , + }, + { + key: ROUTES.ALL_ERROR, + label: 'Exceptions', + icon: , + }, + { + key: ROUTES.SERVICE_MAP, + label: 'Service Map', + icon: , + }, + { + key: ROUTES.USAGE_EXPLORER, + label: 'Usage Explorer', + icon: , + }, + { + key: ROUTES.SETTINGS, + label: 'Settings', + icon: , + }, + { + key: ROUTES.INSTRUMENTATION, + label: 'Get Started', + icon: , + }, +]; + +type SidebarMenu = MenuItem & { + tags?: string[]; +}; + +export default menus; diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index 01ec22677c..f3bcfe560f 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -5,6 +5,7 @@ import { Link, RouteComponentProps, withRouter } from 'react-router-dom'; const breadcrumbNameMap = { [ROUTES.APPLICATION]: 'Services', [ROUTES.TRACE]: 'Traces', + [ROUTES.TRACES_EXPLORER]: 'Traces Explorer', [ROUTES.SERVICE_MAP]: 'Service Map', [ROUTES.USAGE_EXPLORER]: 'Usage Explorer', [ROUTES.INSTRUMENTATION]: 'Get Started', @@ -19,6 +20,7 @@ const breadcrumbNameMap = { [ROUTES.LIST_ALL_ALERT]: 'Alerts', [ROUTES.ALL_DASHBOARD]: 'Dashboard', [ROUTES.LOGS]: 'Logs', + [ROUTES.LOGS_EXPLORER]: 'Logs Explorer', }; function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element { diff --git a/frontend/src/container/TracesExplorer/Controls/index.tsx b/frontend/src/container/TracesExplorer/Controls/index.tsx new file mode 100644 index 0000000000..515fe12c67 --- /dev/null +++ b/frontend/src/container/TracesExplorer/Controls/index.tsx @@ -0,0 +1,25 @@ +import Controls from 'container/Controls'; +import { memo } from 'react'; + +import { Container } from './styles'; + +function TraceExplorerControls(): JSX.Element | null { + const handleCountItemsPerPageChange = (): void => {}; + const handleNavigatePrevious = (): void => {}; + const handleNavigateNext = (): void => {}; + + return ( + + + + ); +} + +export default memo(TraceExplorerControls); diff --git a/frontend/src/container/TracesExplorer/Controls/styles.ts b/frontend/src/container/TracesExplorer/Controls/styles.ts new file mode 100644 index 0000000000..f91bc43363 --- /dev/null +++ b/frontend/src/container/TracesExplorer/Controls/styles.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.5rem; +`; diff --git a/frontend/src/container/TracesExplorer/QuerySection/index.tsx b/frontend/src/container/TracesExplorer/QuerySection/index.tsx new file mode 100644 index 0000000000..169fa7ba79 --- /dev/null +++ b/frontend/src/container/TracesExplorer/QuerySection/index.tsx @@ -0,0 +1,32 @@ +import { Button } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { QueryBuilder } from 'container/QueryBuilder'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { DataSource } from 'types/common/queryBuilder'; + +import { ButtonWrapper, Container } from './styles'; + +function QuerySection(): JSX.Element { + const { handleRunQuery } = useQueryBuilder(); + + return ( + + + + + } + /> + + ); +} + +export default QuerySection; diff --git a/frontend/src/container/TracesExplorer/QuerySection/styles.ts b/frontend/src/container/TracesExplorer/QuerySection/styles.ts new file mode 100644 index 0000000000..cdb46bd580 --- /dev/null +++ b/frontend/src/container/TracesExplorer/QuerySection/styles.ts @@ -0,0 +1,16 @@ +import { Col } from 'antd'; +import Card from 'antd/es/card/Card'; +import styled from 'styled-components'; + +export const Container = styled(Card)` + border: none; + background: inherit; + + .ant-card-body { + padding: 0; + } +`; + +export const ButtonWrapper = styled(Col)` + margin-left: auto; +`; diff --git a/frontend/src/container/TracesExplorer/TimeSeriesView/index.tsx b/frontend/src/container/TracesExplorer/TimeSeriesView/index.tsx new file mode 100644 index 0000000000..ee531d7f40 --- /dev/null +++ b/frontend/src/container/TracesExplorer/TimeSeriesView/index.tsx @@ -0,0 +1,73 @@ +import Graph from 'components/Graph'; +import Spinner from 'components/Spinner'; +import { initialQueriesMap } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import getChartData from 'lib/getChartData'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { Container, ErrorText } from './styles'; + +function TimeSeriesView(): JSX.Element { + const { stagedQuery } = useQueryBuilder(); + + const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const { data, isLoading, isError } = useGetQueryRange( + { + query: stagedQuery || initialQueriesMap.traces, + graphType: 'graph', + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval: globalSelectedTime, + params: { + dataSource: 'traces', + }, + }, + { + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + globalSelectedTime, + stagedQuery, + maxTime, + minTime, + ], + enabled: !!stagedQuery, + }, + ); + + const chartData = useMemo( + () => + getChartData({ + queryData: [ + { + queryData: data?.payload?.data?.result || [], + }, + ], + }), + [data], + ); + + return ( + + {isLoading && } + {isError && {data?.error || 'Something went wrong'}} + {!isLoading && !isError && ( + + )} + + ); +} + +export default TimeSeriesView; diff --git a/frontend/src/container/TracesExplorer/TimeSeriesView/styles.ts b/frontend/src/container/TracesExplorer/TimeSeriesView/styles.ts new file mode 100644 index 0000000000..9f002e047f --- /dev/null +++ b/frontend/src/container/TracesExplorer/TimeSeriesView/styles.ts @@ -0,0 +1,17 @@ +import { Typography } from 'antd'; +import Card from 'antd/es/card/Card'; +import styled from 'styled-components'; + +export const Container = styled(Card)` + position: relative; + margin: 0.5rem 0 3.1rem 0; + + .ant-card-body { + height: 50vh; + min-height: 350px; + } +`; + +export const ErrorText = styled(Typography)` + text-align: center; +`; diff --git a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts new file mode 100644 index 0000000000..4477a9fbf7 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts @@ -0,0 +1,14 @@ +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useMemo } from 'react'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +export const useGetCompositeQueryParam = (): Query | null => { + const urlQuery = useUrlQuery(); + + return useMemo(() => { + const compositeQuery = urlQuery.get(COMPOSITE_QUERY); + + return compositeQuery ? JSON.parse(compositeQuery) : null; + }, [urlQuery]); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts new file mode 100644 index 0000000000..06cc11829a --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts @@ -0,0 +1,16 @@ +import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useMemo } from 'react'; + +export const useGetPanelTypesQueryParam = ( + defaultPanelType?: T, +): T extends undefined ? GRAPH_TYPES | null : GRAPH_TYPES => { + const urlQuery = useUrlQuery(); + + return useMemo(() => { + const panelTypeQuery = urlQuery.get(PANEL_TYPES_QUERY); + + return panelTypeQuery ? JSON.parse(panelTypeQuery) : defaultPanelType; + }, [urlQuery, defaultPanelType]); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts new file mode 100644 index 0000000000..b6e12b517c --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts @@ -0,0 +1,29 @@ +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useMemo } from 'react'; +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; +import { + GetMetricQueryRange, + GetQueryResultsProps, +} from 'store/actions/dashboard/getQueryResults'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; + +type UseGetQueryRange = ( + requestData: GetQueryResultsProps, + options?: UseQueryOptions, Error>, +) => UseQueryResult, Error>; + +export const useGetQueryRange: UseGetQueryRange = (requestData, options) => { + const queryKey = useMemo(() => { + if (options?.queryKey) { + return [...options.queryKey]; + } + return [REACT_QUERY_KEY.GET_QUERY_RANGE, requestData]; + }, [options?.queryKey, requestData]); + + return useQuery, Error>({ + queryFn: async () => GetMetricQueryRange(requestData), + ...options, + queryKey, + }); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts new file mode 100644 index 0000000000..289e6c9f1a --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts @@ -0,0 +1,51 @@ +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; +import { UseQueryOptions, UseQueryResult } from 'react-query'; +import { useSelector } from 'react-redux'; +import { GetQueryResultsProps } from 'store/actions/dashboard/getQueryResults'; +import { AppState } from 'store/reducers'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { useGetQueryRange } from './useGetQueryRange'; + +export const useGetWidgetQueryRange = ( + { + graphType, + selectedTime, + }: Pick, + options?: UseQueryOptions, Error>, +): UseQueryResult, Error> => { + const urlQuery = useUrlQuery(); + + const { selectedTime: globalSelectedInterval } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const compositeQuery = urlQuery.get(COMPOSITE_QUERY); + + return useGetQueryRange( + { + graphType, + selectedTime, + globalSelectedInterval, + query: JSON.parse(compositeQuery || '{}'), + variables: getDashboardVariables(), + }, + { + enabled: !!compositeQuery, + retry: false, + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + selectedTime, + globalSelectedInterval, + compositeQuery, + ], + ...options, + }, + ); +}; diff --git a/frontend/src/hooks/queryBuilder/useQueryOperations.ts b/frontend/src/hooks/queryBuilder/useQueryOperations.ts index 949606c7ad..b1d2f35c18 100644 --- a/frontend/src/hooks/queryBuilder/useQueryOperations.ts +++ b/frontend/src/hooks/queryBuilder/useQueryOperations.ts @@ -1,6 +1,6 @@ import { initialAutocompleteData, - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, mapOfFilters, } from 'constants/queryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; @@ -21,6 +21,7 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => { handleSetQueryData, removeQueryBuilderEntityByIndex, panelType, + initialDataSource, } = useQueryBuilder(); const [operators, setOperators] = useState[]>([]); const [listOfAdditionalFilters, setListOfAdditionalFilters] = useState< @@ -80,9 +81,9 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => { panelType, }); - const entries = Object.entries(initialQueryBuilderFormValues).filter( - ([key]) => key !== 'queryName' && key !== 'expression', - ); + const entries = Object.entries( + initialQueryBuilderFormValuesMap.metrics, + ).filter(([key]) => key !== 'queryName' && key !== 'expression'); const initCopyResult = Object.fromEntries(entries); @@ -121,12 +122,32 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => { ); useEffect(() => { + if (initialDataSource && dataSource !== initialDataSource) return; + const initialOperators = getOperatorsBySourceAndPanelType({ dataSource, panelType, }); + + if (JSON.stringify(operators) === JSON.stringify(initialOperators)) return; + setOperators(initialOperators); - }, [dataSource, panelType]); + + const isCurrentOperatorAvailableInList = initialOperators + .map((operator) => operator.value) + .includes(aggregateOperator); + + if (!isCurrentOperatorAvailableInList) { + handleChangeOperator(initialOperators[0].value); + } + }, [ + dataSource, + initialDataSource, + panelType, + operators, + aggregateOperator, + handleChangeOperator, + ]); useEffect(() => { const additionalFilters = getNewListOfAdditionalFilters(dataSource); diff --git a/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts b/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts index f6f34fac39..168e5af77a 100644 --- a/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts +++ b/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts @@ -1,27 +1,19 @@ -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import useUrlQuery from 'hooks/useUrlQuery'; -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { useGetCompositeQueryParam } from './useGetCompositeQueryParam'; import { useQueryBuilder } from './useQueryBuilder'; type UseShareBuilderUrlParams = { defaultValue: Query }; -type UseShareBuilderUrlReturnType = { compositeQuery: Query | null }; export const useShareBuilderUrl = ({ defaultValue, -}: UseShareBuilderUrlParams): UseShareBuilderUrlReturnType => { - const { redirectWithQueryBuilderData } = useQueryBuilder(); +}: UseShareBuilderUrlParams): void => { + const { redirectWithQueryBuilderData, resetStagedQuery } = useQueryBuilder(); const urlQuery = useUrlQuery(); - const compositeQuery: Query | null = useMemo(() => { - const query = urlQuery.get(COMPOSITE_QUERY); - if (query) { - return JSON.parse(query); - } - - return null; - }, [urlQuery]); + const compositeQuery = useGetCompositeQueryParam(); useEffect(() => { if (!compositeQuery) { @@ -29,5 +21,10 @@ export const useShareBuilderUrl = ({ } }, [defaultValue, urlQuery, redirectWithQueryBuilderData, compositeQuery]); - return { compositeQuery }; + useEffect( + () => (): void => { + resetStagedQuery(); + }, + [resetStagedQuery], + ); }; diff --git a/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts index 05707b5fa1..e9f4f1c6e1 100644 --- a/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts +++ b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts @@ -1,5 +1,4 @@ -import GetMinMax from 'lib/getMinMax'; -import GetStartAndEndTime from 'lib/getStartAndEndTime'; +import getStartEndRangeTime from 'lib/getStartEndRangeTime'; import store from 'store'; export const getDashboardVariables = (): Record => { @@ -13,16 +12,11 @@ export const getDashboardVariables = (): Record => { data: { variables = {} }, } = selectedDashboard; - const minMax = GetMinMax(globalTime.selectedTime, [ - globalTime.minTime / 1000000, - globalTime.maxTime / 1000000, - ]); - - const { start, end } = GetStartAndEndTime({ + const { start, end } = getStartEndRangeTime({ type: 'GLOBAL_TIME', - minTime: minMax.minTime, - maxTime: minMax.maxTime, + interval: globalTime.selectedTime, }); + const variablesTuple: Record = { SIGNOZ_START_TIME: parseInt(start, 10) * 1e3, SIGNOZ_END_TIME: parseInt(end, 10) * 1e3, diff --git a/frontend/src/lib/explorer/getExplorerChartData.ts b/frontend/src/lib/explorer/getExplorerChartData.ts new file mode 100644 index 0000000000..152b72f9ac --- /dev/null +++ b/frontend/src/lib/explorer/getExplorerChartData.ts @@ -0,0 +1,46 @@ +import { ChartData } from 'chart.js'; +import getLabelName from 'lib/getLabelName'; +import { QueryData } from 'types/api/widgets/getQuery'; + +import { colors } from '../getRandomColor'; + +export const getExplorerChartData = ( + queryData: QueryData[], +): ChartData<'bar'> => { + const uniqueTimeLabels = new Set(); + + const sortedData = [...queryData].sort((a, b) => { + if (a.queryName < b.queryName) return -1; + if (a.queryName > b.queryName) return 1; + return 0; + }); + + const modifiedData: { label: string }[] = sortedData.map((result) => { + const { metric, queryName, legend } = result; + result.values.forEach((value) => { + uniqueTimeLabels.add(value[0] * 1000); + }); + + return { + label: getLabelName(metric, queryName || '', legend || ''), + }; + }); + + const labels = Array.from(uniqueTimeLabels) + .sort((a, b) => a - b) + .map((value) => new Date(value)); + + const allLabels = modifiedData.map((e) => e.label); + + const data: ChartData<'bar'> = { + labels, + datasets: queryData.map((result, index) => ({ + label: allLabels[index], + data: result.values.map((item) => parseFloat(item[1])), + backgroundColor: colors[index % colors.length] || 'red', + borderColor: colors[index % colors.length] || 'red', + })), + }; + + return data; +}; diff --git a/frontend/src/lib/getChartData.ts b/frontend/src/lib/getChartData.ts index 97e16dd895..8d32d969c2 100644 --- a/frontend/src/lib/getChartData.ts +++ b/frontend/src/lib/getChartData.ts @@ -1,6 +1,6 @@ import { ChartData } from 'chart.js'; import getLabelName from 'lib/getLabelName'; -import { Widgets } from 'types/api/dashboard/getAll'; +import { QueryData } from 'types/api/widgets/getQuery'; import convertIntoEpoc from './covertIntoEpoc'; import { colors } from './getRandomColor'; @@ -77,7 +77,11 @@ const getChartData = ({ queryData }: GetChartDataProps): ChartData => { }; interface GetChartDataProps { - queryData: Widgets['queryData']['data'][]; + queryData: { + query?: string; + legend?: string; + queryData: QueryData[]; + }[]; } export default getChartData; diff --git a/frontend/src/lib/getStartEndRangeTime.ts b/frontend/src/lib/getStartEndRangeTime.ts new file mode 100644 index 0000000000..486b6d0784 --- /dev/null +++ b/frontend/src/lib/getStartEndRangeTime.ts @@ -0,0 +1,48 @@ +import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; +import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; +import { Time } from 'container/TopNav/DateTimeSelection/config'; +import store from 'store'; + +import getMaxMinTime from './getMaxMinTime'; +import getMinMax from './getMinMax'; +import getStartAndEndTime from './getStartAndEndTime'; + +const getStartEndRangeTime = ({ + type = 'GLOBAL_TIME', + graphType = null, + interval = 'custom', +}: GetStartEndRangeTimesProps): GetStartEndRangeTimesPayload => { + const { globalTime } = store.getState(); + + const minMax = getMinMax(interval, [ + globalTime.minTime / 1000000, + globalTime.maxTime / 1000000, + ]); + + const maxMinTime = getMaxMinTime({ + graphType, + maxTime: minMax.maxTime, + minTime: minMax.minTime, + }); + + const { end, start } = getStartAndEndTime({ + type, + maxTime: maxMinTime.maxTime, + minTime: maxMinTime.minTime, + }); + + return { start, end }; +}; + +interface GetStartEndRangeTimesProps { + type?: timePreferenceType; + graphType?: ITEMS | null; + interval?: Time; +} + +interface GetStartEndRangeTimesPayload { + start: string; + end: string; +} + +export default getStartEndRangeTime; diff --git a/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts b/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts index d9a9753d6e..21b3bc7498 100644 --- a/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts +++ b/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts @@ -15,6 +15,11 @@ export const getOperatorsBySourceAndPanelType = ({ }: GetQueryOperatorsParams): SelectOption[] => { let operatorsByDataSource = mapOfOperators[dataSource]; + if (panelType === PANEL_TYPES.LIST) { + operatorsByDataSource = operatorsByDataSource.filter( + (operator) => operator.value === StringOperators.NOOP, + ); + } if (dataSource !== DataSource.METRICS && panelType !== PANEL_TYPES.LIST) { operatorsByDataSource = operatorsByDataSource.filter( (operator) => operator.value !== StringOperators.NOOP, diff --git a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts index 860b2d1266..f30bfc13b7 100644 --- a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts +++ b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts @@ -1,6 +1,7 @@ -import { initialQuery } from 'constants/queryBuilder'; +import { initialQueryState } from 'constants/queryBuilder'; import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { v4 as uuid } from 'uuid'; import { transformQueryBuilderDataModel } from '../transformQueryBuilderDataModel'; @@ -9,14 +10,14 @@ export const mapQueryDataFromApi = ( ): Query => { const builder = compositeQuery.builderQueries ? transformQueryBuilderDataModel(compositeQuery.builderQueries) - : initialQuery.builder; + : initialQueryState.builder; const promql = compositeQuery.promQueries ? Object.keys(compositeQuery.promQueries).map((key) => ({ ...compositeQuery.promQueries[key], name: key, })) - : initialQuery.promql; + : initialQueryState.promql; const clickhouseSql = compositeQuery.chQueries ? Object.keys(compositeQuery.chQueries).map((key) => ({ @@ -24,12 +25,13 @@ export const mapQueryDataFromApi = ( name: key, query: compositeQuery.chQueries[key].query, })) - : initialQuery.clickhouse_sql; + : initialQueryState.clickhouse_sql; return { builder, promql, clickhouse_sql: clickhouseSql, queryType: compositeQuery.queryType, + id: uuid(), }; }; diff --git a/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts b/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts index 784ac41921..3cf545d49b 100644 --- a/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts +++ b/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts @@ -1,6 +1,6 @@ import { initialFormulaBuilderFormValues, - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, } from 'constants/queryBuilder'; import { FORMULA_REGEXP } from 'constants/regExp'; import { @@ -22,7 +22,7 @@ export const transformQueryBuilderDataModel = ( queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula }); } else { const query = value as IBuilderQuery; - queryData.push({ ...initialQueryBuilderFormValues, ...query }); + queryData.push({ ...initialQueryBuilderFormValuesMap.metrics, ...query }); } }); diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx new file mode 100644 index 0000000000..fb8685717c --- /dev/null +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -0,0 +1,45 @@ +import { Button, Col, Row } from 'antd'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { LogsExplorerChart } from 'container/LogsExplorerChart'; +import { LogsExplorerViews } from 'container/LogsExplorerViews'; +import { QueryBuilder } from 'container/QueryBuilder'; +import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; +import { DataSource } from 'types/common/queryBuilder'; + +// ** Styles +import { ButtonWrapperStyled, WrapperStyled } from './styles'; + +function LogsExporer(): JSX.Element { + const { handleRunQuery } = useQueryBuilder(); + const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); + + useShareBuilderUrl({ defaultValue: initialQueriesMap.logs }); + + return ( + + + + + + + } + /> + + + + + + + + ); +} + +export default LogsExporer; diff --git a/frontend/src/pages/LogsExplorer/styles.ts b/frontend/src/pages/LogsExplorer/styles.ts new file mode 100644 index 0000000000..3e479cc001 --- /dev/null +++ b/frontend/src/pages/LogsExplorer/styles.ts @@ -0,0 +1,11 @@ +import { Col } from 'antd'; +import { themeColors } from 'constants/theme'; +import styled from 'styled-components'; + +export const WrapperStyled = styled.div` + color: ${themeColors.lightWhite}; +`; + +export const ButtonWrapperStyled = styled(Col)` + margin-left: auto; +`; diff --git a/frontend/src/pages/NewDashboard/index.tsx b/frontend/src/pages/NewDashboard/index.tsx index 2183531f63..6d64ca500f 100644 --- a/frontend/src/pages/NewDashboard/index.tsx +++ b/frontend/src/pages/NewDashboard/index.tsx @@ -1,3 +1,5 @@ +import { Typography } from 'antd'; +import NotFound from 'components/NotFound'; import Spinner from 'components/Spinner'; import NewDashboard from 'container/NewDashboard'; import { useEffect } from 'react'; @@ -8,6 +10,7 @@ import { ThunkDispatch } from 'redux-thunk'; import { GetDashboard, GetDashboardProps } from 'store/actions/dashboard'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; +import { ErrorType } from 'types/common'; import DashboardReducer from 'types/reducer/dashboards'; function NewDashboardPage({ getDashboard }: NewDashboardProps): JSX.Element { @@ -26,8 +29,17 @@ function NewDashboardPage({ getDashboard }: NewDashboardProps): JSX.Element { } }, [getDashboard, dashboardId, dashboards.length]); + if ( + error && + !loading && + dashboards.length === 0 && + errorMessage === ErrorType.NotFound + ) { + return ; + } + if (error && !loading && dashboards.length === 0) { - return
{errorMessage}
; + return {errorMessage}; } // when user comes from dashboard page. dashboard array is populated with some dashboard as dashboard is populated diff --git a/frontend/src/pages/TracesExplorer/constants.ts b/frontend/src/pages/TracesExplorer/constants.ts new file mode 100644 index 0000000000..ceea4e582e --- /dev/null +++ b/frontend/src/pages/TracesExplorer/constants.ts @@ -0,0 +1,6 @@ +export const CURRENT_TRACES_EXPLORER_TAB = 'currentTab'; + +export enum TracesExplorerTabs { + TIME_SERIES = 'times-series', + TRACES = 'traces', +} diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx new file mode 100644 index 0000000000..57c2310d78 --- /dev/null +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -0,0 +1,59 @@ +import { Tabs } from 'antd'; +import { initialQueriesMap } from 'constants/queryBuilder'; +import QuerySection from 'container/TracesExplorer/QuerySection'; +import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useCallback, useEffect } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import { CURRENT_TRACES_EXPLORER_TAB, TracesExplorerTabs } from './constants'; +import { Container } from './styles'; +import { getTabsItems } from './utils'; + +function TracesExplorer(): JSX.Element { + const urlQuery = useUrlQuery(); + const history = useHistory(); + const location = useLocation(); + + const currentUrlTab = urlQuery.get( + CURRENT_TRACES_EXPLORER_TAB, + ) as TracesExplorerTabs; + const currentTab = currentUrlTab || TracesExplorerTabs.TIME_SERIES; + const tabsItems = getTabsItems(); + + const redirectWithCurrentTab = useCallback( + (tabKey: string): void => { + urlQuery.set(CURRENT_TRACES_EXPLORER_TAB, tabKey); + const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; + history.push(generatedUrl); + }, + [history, location, urlQuery], + ); + + const handleTabChange = useCallback( + (tabKey: string): void => { + redirectWithCurrentTab(tabKey); + }, + [redirectWithCurrentTab], + ); + + useShareBuilderUrl({ defaultValue: initialQueriesMap.traces }); + + useEffect(() => { + if (currentUrlTab) return; + + redirectWithCurrentTab(TracesExplorerTabs.TIME_SERIES); + }, [currentUrlTab, redirectWithCurrentTab]); + + return ( + <> + + + + + + + ); +} + +export default TracesExplorer; diff --git a/frontend/src/pages/TracesExplorer/styles.ts b/frontend/src/pages/TracesExplorer/styles.ts new file mode 100644 index 0000000000..6da55b8d4d --- /dev/null +++ b/frontend/src/pages/TracesExplorer/styles.ts @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + margin: 1rem 0; +`; diff --git a/frontend/src/pages/TracesExplorer/utils.tsx b/frontend/src/pages/TracesExplorer/utils.tsx new file mode 100644 index 0000000000..aff29bba58 --- /dev/null +++ b/frontend/src/pages/TracesExplorer/utils.tsx @@ -0,0 +1,17 @@ +import { TabsProps } from 'antd'; +import TimeSeriesView from 'container/TracesExplorer/TimeSeriesView'; + +import { TracesExplorerTabs } from './constants'; + +export const getTabsItems = (): TabsProps['items'] => [ + { + label: 'Time Series', + key: TracesExplorerTabs.TIME_SERIES, + children: , + }, + { + label: 'Traces', + key: TracesExplorerTabs.TRACES, + children:
Traces tab
, + }, +]; diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 7064af91d2..2389ca01bc 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -4,10 +4,10 @@ import { formulasNames, initialClickHouseData, initialFormulaBuilderFormValues, - initialQuery, - initialQueryBuilderFormValues, + initialQueriesMap, + initialQueryBuilderFormValuesMap, initialQueryPromQLData, - initialQueryWithType, + initialQueryState, initialSingleQueryMap, MAX_FORMULAS, MAX_QUERIES, @@ -15,6 +15,7 @@ import { } from 'constants/queryBuilder'; import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; import useUrlQuery from 'hooks/useUrlQuery'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; @@ -44,19 +45,17 @@ import { QueryBuilderContextType, QueryBuilderData, } from 'types/common/queryBuilder'; +import { v4 as uuid } from 'uuid'; export const QueryBuilderContext = createContext({ - currentQuery: initialQueryWithType, + currentQuery: initialQueriesMap.metrics, + stagedQuery: initialQueriesMap.metrics, initialDataSource: null, panelType: PANEL_TYPES.TIME_SERIES, - resetQueryBuilderData: () => {}, - resetQueryBuilderInfo: () => {}, handleSetQueryData: () => {}, handleSetFormulaData: () => {}, handleSetQueryItemData: () => {}, handleSetPanelType: () => {}, - handleSetQueryType: () => {}, - initQueryBuilderData: () => {}, setupInitialDataSource: () => {}, removeQueryBuilderEntityByIndex: () => {}, removeQueryTypeItemByIndex: () => {}, @@ -64,6 +63,8 @@ export const QueryBuilderContext = createContext({ addNewFormula: () => {}, addNewQueryItem: () => {}, redirectWithQueryBuilderData: () => {}, + handleRunQuery: () => {}, + resetStagedQuery: () => {}, }); export function QueryBuilderProvider({ @@ -73,6 +74,8 @@ export function QueryBuilderProvider({ const history = useHistory(); const location = useLocation(); + const compositeQueryParam = useGetCompositeQueryParam(); + const [initialDataSource, setInitialDataSource] = useState( null, ); @@ -81,81 +84,77 @@ export function QueryBuilderProvider({ PANEL_TYPES.TIME_SERIES, ); - const [currentQuery, setCurrentQuery] = useState(initialQuery); + const [currentQuery, setCurrentQuery] = useState( + initialQueryState, + ); + const [stagedQuery, setStagedQuery] = useState(null); const [queryType, setQueryType] = useState( EQueryType.QUERY_BUILDER, ); - const handleSetQueryType = useCallback((newQueryType: EQueryType) => { - setQueryType(newQueryType); - }, []); + const initQueryBuilderData = useCallback( + (query: Query): void => { + const { queryType: newQueryType, ...queryState } = query; - const resetQueryBuilderInfo = useCallback((): void => { - setInitialDataSource(null); - setPanelType(PANEL_TYPES.TIME_SERIES); - }, []); - - const resetQueryBuilderData = useCallback(() => { - setCurrentQuery(initialQuery); - }, []); - - const initQueryBuilderData = useCallback((query: Partial): void => { - const { queryType, ...queryState } = query; - - const builder: QueryBuilderData = { - queryData: queryState.builder - ? queryState.builder.queryData.map((item) => ({ - ...initialQueryBuilderFormValues, - ...item, - })) - : initialQuery.builder.queryData, - queryFormulas: queryState.builder - ? queryState.builder.queryFormulas.map((item) => ({ - ...initialFormulaBuilderFormValues, - ...item, - })) - : initialQuery.builder.queryFormulas, - }; - - const promql: IPromQLQuery[] = queryState.promql - ? queryState.promql.map((item) => ({ - ...initialQueryPromQLData, + const builder: QueryBuilderData = { + queryData: queryState.builder.queryData.map((item) => ({ + ...initialQueryBuilderFormValuesMap[ + initialDataSource || DataSource.METRICS + ], ...item, - })) - : initialQuery.promql; + })), + queryFormulas: queryState.builder.queryFormulas.map((item) => ({ + ...initialFormulaBuilderFormValues, + ...item, + })), + }; - const clickHouse: IClickHouseQuery[] = queryState.clickhouse_sql - ? queryState.clickhouse_sql.map((item) => ({ + const promql: IPromQLQuery[] = queryState.promql.map((item) => ({ + ...initialQueryPromQLData, + ...item, + })); + + const clickHouse: IClickHouseQuery[] = queryState.clickhouse_sql.map( + (item) => ({ ...initialClickHouseData, ...item, - })) - : initialQuery.clickhouse_sql; + }), + ); - setCurrentQuery({ - clickhouse_sql: clickHouse, - promql, - builder: { - ...builder, - queryData: builder.queryData.map((q) => ({ - ...q, - groupBy: q.groupBy.map(({ id: _, ...item }) => ({ - ...item, - id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder), + const type = newQueryType || EQueryType.QUERY_BUILDER; + + const newQueryState: QueryState = { + clickhouse_sql: clickHouse, + promql, + builder: { + ...builder, + queryData: builder.queryData.map((q) => ({ + ...q, + groupBy: q.groupBy.map(({ id: _, ...item }) => ({ + ...item, + id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder), + })), + aggregateAttribute: { + ...q.aggregateAttribute, + id: createIdFromObjectFields( + q.aggregateAttribute, + baseAutoCompleteIdKeysOrder, + ), + }, })), - aggregateAttribute: { - ...q.aggregateAttribute, - id: createIdFromObjectFields( - q.aggregateAttribute, - baseAutoCompleteIdKeysOrder, - ), - }, - })), - }, - }); + }, + id: queryState.id, + }; - setQueryType(queryType || EQueryType.QUERY_BUILDER); - }, []); + const nextQuery: Query = { ...newQueryState, queryType: type }; + + setStagedQuery(nextQuery); + setCurrentQuery(newQueryState); + setQueryType(type); + }, + [initialDataSource], + ); const removeQueryBuilderEntityByIndex = useCallback( (type: keyof QueryBuilderData, index: number) => { @@ -190,9 +189,11 @@ export function QueryBuilderProvider({ const createNewBuilderQuery = useCallback( (queries: IBuilderQuery[]): IBuilderQuery => { const existNames = queries.map((item) => item.queryName); + const initialBuilderQuery = + initialQueryBuilderFormValuesMap[initialDataSource || DataSource.METRICS]; const newQuery: IBuilderQuery = { - ...initialQueryBuilderFormValues, + ...initialBuilderQuery, queryName: createNewBuilderItemName({ existNames, sourceNames: alphabet }), expression: createNewBuilderItemName({ existNames, @@ -381,7 +382,7 @@ export function QueryBuilderProvider({ }, []); const redirectWithQueryBuilderData = useCallback( - (query: Partial) => { + (query: Partial, searchParams?: Record) => { const currentGeneratedQuery: Query = { queryType: !query.queryType || !Object.values(EQueryType).includes(query.queryType) @@ -389,63 +390,84 @@ export function QueryBuilderProvider({ : query.queryType, builder: !query.builder || query.builder.queryData.length === 0 - ? initialQuery.builder + ? initialQueryState.builder : query.builder, promql: !query.promql || query.promql.length === 0 - ? initialQuery.promql + ? initialQueryState.promql : query.promql, clickhouse_sql: !query.clickhouse_sql || query.clickhouse_sql.length === 0 - ? initialQuery.clickhouse_sql + ? initialQueryState.clickhouse_sql : query.clickhouse_sql, + id: uuid(), }; urlQuery.set(COMPOSITE_QUERY, JSON.stringify(currentGeneratedQuery)); - const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; + if (searchParams) { + Object.keys(searchParams).forEach((param) => + urlQuery.set(param, JSON.stringify(searchParams[param])), + ); + } + + const generatedUrl = `${location.pathname}?${urlQuery}`; history.push(generatedUrl); }, [history, location, urlQuery], ); - useEffect(() => { - const compositeQuery = urlQuery.get(COMPOSITE_QUERY); - if (!compositeQuery) return; + const handleRunQuery = useCallback(() => { + redirectWithQueryBuilderData({ ...currentQuery, queryType }); + }, [redirectWithQueryBuilderData, currentQuery, queryType]); - const newQuery: Query = JSON.parse(compositeQuery); + const resetStagedQuery = useCallback(() => { + setStagedQuery(null); + }, []); + + useEffect(() => { + if (!compositeQueryParam) return; + + if (stagedQuery && stagedQuery.id === compositeQueryParam.id) { + return; + } const { isValid, validData } = replaceIncorrectObjectFields( - newQuery, - initialQueryWithType, + compositeQueryParam, + initialQueriesMap.metrics, ); if (!isValid) { redirectWithQueryBuilderData(validData); } else { - initQueryBuilderData(newQuery); + initQueryBuilderData(compositeQueryParam); } - }, [initQueryBuilderData, redirectWithQueryBuilderData, urlQuery]); - - const query: Query = useMemo(() => ({ ...currentQuery, queryType }), [ - currentQuery, - queryType, + }, [ + initQueryBuilderData, + redirectWithQueryBuilderData, + compositeQueryParam, + stagedQuery, ]); + const query: Query = useMemo( + () => ({ + ...currentQuery, + queryType, + }), + [currentQuery, queryType], + ); + const contextValues: QueryBuilderContextType = useMemo( () => ({ currentQuery: query, + stagedQuery, initialDataSource, panelType, - resetQueryBuilderData, - resetQueryBuilderInfo, handleSetQueryData, handleSetFormulaData, handleSetQueryItemData, handleSetPanelType, - handleSetQueryType, - initQueryBuilderData, setupInitialDataSource, removeQueryBuilderEntityByIndex, removeQueryTypeItemByIndex, @@ -453,19 +475,18 @@ export function QueryBuilderProvider({ addNewFormula, addNewQueryItem, redirectWithQueryBuilderData, + handleRunQuery, + resetStagedQuery, }), [ query, + stagedQuery, initialDataSource, panelType, - resetQueryBuilderData, - resetQueryBuilderInfo, handleSetQueryData, handleSetFormulaData, handleSetQueryItemData, handleSetPanelType, - handleSetQueryType, - initQueryBuilderData, setupInitialDataSource, removeQueryBuilderEntityByIndex, removeQueryTypeItemByIndex, @@ -473,6 +494,8 @@ export function QueryBuilderProvider({ addNewFormula, addNewQueryItem, redirectWithQueryBuilderData, + handleRunQuery, + resetStagedQuery, ], ); diff --git a/frontend/src/store/actions/dashboard/getDashboard.ts b/frontend/src/store/actions/dashboard/getDashboard.ts index 4ca70c38d9..f84461c655 100644 --- a/frontend/src/store/actions/dashboard/getDashboard.ts +++ b/frontend/src/store/actions/dashboard/getDashboard.ts @@ -1,5 +1,5 @@ import getDashboard from 'api/dashboard/get'; -import { initialQueryWithType, PANEL_TYPES } from 'constants/queryBuilder'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { Dispatch } from 'redux'; import AppActions from 'types/actions'; @@ -39,17 +39,7 @@ export const GetDashboard = ({ panelTypes: graphType || PANEL_TYPES.TIME_SERIES, timePreferance: 'GLOBAL_TIME', title: '', - queryType: 0, - queryData: { - data: { - queryData: [], - }, - - error: false, - errorMessage: '', - loading: false, - }, - query: initialQueryWithType, + query: initialQueriesMap.metrics, }, }); } diff --git a/frontend/src/store/actions/dashboard/getQueryResults.ts b/frontend/src/store/actions/dashboard/getQueryResults.ts index 8f87e8cb49..c139c232c3 100644 --- a/frontend/src/store/actions/dashboard/getQueryResults.ts +++ b/frontend/src/store/actions/dashboard/getQueryResults.ts @@ -3,26 +3,18 @@ // @ts-nocheck import { getMetricsQueryRange } from 'api/metrics/getQueryRange'; -import { AxiosError } from 'axios'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; -import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Time } from 'container/TopNav/DateTimeSelection/config'; -import GetMaxMinTime from 'lib/getMaxMinTime'; -import GetMinMax from 'lib/getMinMax'; -import GetStartAndEndTime from 'lib/getStartAndEndTime'; import getStep from 'lib/getStep'; import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi'; import { isEmpty } from 'lodash-es'; -import { Dispatch } from 'redux'; -import store from 'store'; -import AppActions from 'types/actions'; -import { ErrorResponse, SuccessResponse } from 'types/api'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { SuccessResponse } from 'types/api'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { EQueryType } from 'types/common/dashboard'; -import { GlobalReducer } from 'types/reducer/globalTime'; import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld'; +import getStartEndRangeTime from 'lib/getStartEndRangeTime'; export async function GetMetricQueryRange({ query, @@ -30,13 +22,8 @@ export async function GetMetricQueryRange({ graphType, selectedTime, variables = {}, -}: { - query: Query; - graphType: GRAPH_TYPES; - selectedTime: timePreferenceType; - globalSelectedInterval: Time; - variables?: Record; -}): Promise | ErrorResponse> { + params = {}, +}: GetQueryResultsProps): Promise> { const queryData = query[query.queryType]; let legendMap: Record = {}; @@ -94,30 +81,18 @@ export async function GetMetricQueryRange({ return; } - const { globalTime } = store.getState(); - - const minMax = GetMinMax(globalSelectedInterval, [ - globalTime.minTime / 1000000, - globalTime.maxTime / 1000000, - ]); - - const getMaxMinTime = GetMaxMinTime({ - graphType: null, - maxTime: minMax.maxTime, - minTime: minMax.minTime, - }); - - const { end, start } = GetStartAndEndTime({ + const { start, end } = getStartEndRangeTime({ type: selectedTime, - maxTime: getMaxMinTime.maxTime, - minTime: getMaxMinTime.minTime, + interval: globalSelectedInterval, }); + const response = await getMetricsQueryRange({ start: parseInt(start, 10) * 1e3, end: parseInt(end, 10) * 1e3, step: getStep({ start, end, inputFormat: 'ms' }), variables, ...QueryPayload, + ...params, }); if (response.statusCode >= 400) { throw new Error( @@ -153,66 +128,11 @@ export async function GetMetricQueryRange({ return response; } -export const GetQueryResults = ( - props: GetQueryResultsProps, -): ((dispatch: Dispatch) => void) => { - return async (dispatch: Dispatch): Promise => { - try { - dispatch({ - type: 'QUERY_ERROR', - payload: { - errorMessage: '', - widgetId: props.widgetId, - errorBoolean: false, - isLoadingQueryResult: true, - }, - }); - const response = await GetMetricQueryRange(props); - - const isError = response.error; - - if (isError != null) { - dispatch({ - type: 'QUERY_ERROR', - payload: { - errorMessage: isError || '', - widgetId: props.widgetId, - isLoadingQueryResult: false, - }, - }); - return; - } - - dispatch({ - type: 'QUERY_SUCCESS', - payload: { - widgetId: props.widgetId, - data: { - queryData: response.payload?.data?.result - ? response.payload?.data?.result - : [], - }, - }, - }); - } catch (error) { - dispatch({ - type: 'QUERY_ERROR', - payload: { - errorMessage: (error as AxiosError).toString(), - widgetId: props.widgetId, - errorBoolean: true, - isLoadingQueryResult: false, - }, - }); - } - }; -}; - export interface GetQueryResultsProps { - widgetId: string; - selectedTime: timePreferenceType; query: Query; - graphType: ITEMS; - globalSelectedInterval: GlobalReducer['selectedTime']; - variables: Record; + graphType: GRAPH_TYPES; + selectedTime: timePreferenceType; + globalSelectedInterval: Time; + variables?: Record; + params?: Record; } diff --git a/frontend/src/store/actions/dashboard/saveDashboard.ts b/frontend/src/store/actions/dashboard/saveDashboard.ts index 5ec83da579..107fecee25 100644 --- a/frontend/src/store/actions/dashboard/saveDashboard.ts +++ b/frontend/src/store/actions/dashboard/saveDashboard.ts @@ -1,5 +1,7 @@ +import { notification } from 'antd'; import updateDashboardApi from 'api/dashboard/update'; import { AxiosError } from 'axios'; +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import history from 'lib/history'; @@ -84,6 +86,12 @@ export const SaveDashboard = ({ ]; }; const allLayout = getAllLayout(); + const params = new URLSearchParams(window.location.search); + const compositeQuery = params.get(COMPOSITE_QUERY); + const query = compositeQuery + ? JSON.parse(compositeQuery) + : selectedWidget.query; + const response = await updateDashboardApi({ data: { ...selectedDashboard.data, @@ -98,6 +106,7 @@ export const SaveDashboard = ({ ...preWidget, { ...selectedWidget, + query, description: updatedDescription, id: isEmptyWidget ? newWidgetId : widgetId, isStacked: updatedisStacked, @@ -107,9 +116,6 @@ export const SaveDashboard = ({ timePreferance: updatedtimePreferance, yAxisUnit: updatedYAxisUnit, panelTypes: graphType, - queryData: { - ...selectedWidget.queryData, - }, }, ...afterWidget, ], @@ -124,10 +130,16 @@ export const SaveDashboard = ({ }); history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); } else { + const error = 'Something went wrong'; + + notification.error({ + message: response.error || error, + }); + dispatch({ type: 'SAVE_SETTING_TO_PANEL_ERROR', payload: { - errorMessage: response.error || 'Something went wrong', + errorMessage: response.error || error, }, }); } diff --git a/frontend/src/store/actions/dashboard/updateQuery.ts b/frontend/src/store/actions/dashboard/updateQuery.ts index e2551e60e2..5b30c4dfe3 100644 --- a/frontend/src/store/actions/dashboard/updateQuery.ts +++ b/frontend/src/store/actions/dashboard/updateQuery.ts @@ -1,6 +1,5 @@ import { Dispatch } from 'redux'; import AppActions from 'types/actions'; -import { Query } from 'types/api/queryBuilder/queryBuilderData'; export const UpdateQuery = ( props: UpdateQueryProps, @@ -10,7 +9,6 @@ export const UpdateQuery = ( dispatch({ type: 'UPDATE_QUERY', payload: { - query: props.updatedQuery, widgetId: props.widgetId, yAxisUnit: props.yAxisUnit, }, @@ -18,7 +16,6 @@ export const UpdateQuery = ( }; export interface UpdateQueryProps { - updatedQuery: Query; widgetId: string; yAxisUnit: string | undefined; } diff --git a/frontend/src/store/reducers/dashboard.ts b/frontend/src/store/reducers/dashboard.ts index 227d34c7d3..f9140830b3 100644 --- a/frontend/src/store/reducers/dashboard.ts +++ b/frontend/src/store/reducers/dashboard.ts @@ -13,8 +13,6 @@ import { GET_DASHBOARD_LOADING_START, GET_DASHBOARD_SUCCESS, IS_ADD_WIDGET, - QUERY_ERROR, - QUERY_SUCCESS, SAVE_SETTING_TO_PANEL_SUCCESS, TOGGLE_EDIT_MODE, UPDATE_DASHBOARD, @@ -30,9 +28,7 @@ const InitialValue: InitialValueTypes = { error: false, errorMessage: '', isEditMode: false, - isQueryFired: false, isAddWidget: false, - isLoadingQueryResult: false, }; const dashboard = ( @@ -170,102 +166,6 @@ const dashboard = ( }; } - case QUERY_ERROR: { - const { - widgetId, - errorMessage, - errorBoolean = true, - isLoadingQueryResult = false, - } = action.payload; - const [selectedDashboard] = state.dashboards; - const { data } = selectedDashboard; - - const selectedWidgetIndex = data.widgets?.findIndex( - (e) => e.id === widgetId, - ); - const { widgets } = data; - - const preWidget = data.widgets?.slice(0, selectedWidgetIndex); - const afterWidget = data.widgets?.slice( - (selectedWidgetIndex || 0) + 1, // this is never undefined - widgets?.length, - ); - const selectedWidget = - (selectedDashboard.data.widgets || [])[selectedWidgetIndex || 0] || {}; - - return { - ...state, - dashboards: [ - { - ...selectedDashboard, - data: { - ...data, - widgets: [ - ...(preWidget || []), - { - ...selectedWidget, - queryData: { - ...selectedWidget.queryData, - error: errorBoolean, - errorMessage, - }, - }, - ...(afterWidget || []), - ], - }, - }, - ], - isQueryFired: true, - isLoadingQueryResult, - }; - } - - case QUERY_SUCCESS: { - const { widgetId, data: queryDataResponse } = action.payload; - - const { dashboards } = state; - const [selectedDashboard] = dashboards; - const { data } = selectedDashboard; - const { widgets = [] } = data; - - const selectedWidgetIndex = widgets.findIndex((e) => e.id === widgetId) || 0; - - const preWidget = widgets?.slice(0, selectedWidgetIndex) || []; - const afterWidget = - widgets.slice( - selectedWidgetIndex + 1, // this is never undefined - widgets.length, - ) || []; - const selectedWidget = widgets[selectedWidgetIndex]; - - return { - ...state, - dashboards: [ - { - ...selectedDashboard, - data: { - ...data, - widgets: [ - ...preWidget, - { - ...selectedWidget, - queryData: { - data: queryDataResponse, - error: selectedWidget.queryData.error, - errorMessage: selectedWidget.queryData.errorMessage, - loading: false, - }, - }, - ...afterWidget, - ], - }, - }, - ], - isQueryFired: true, - isLoadingQueryResult: false, - }; - } - case APPLY_SETTINGS_TO_PANEL: { const { widgetId } = action.payload; @@ -367,7 +267,7 @@ const dashboard = ( } case UPDATE_QUERY: { - const { query, widgetId, yAxisUnit } = action.payload; + const { widgetId, yAxisUnit } = action.payload; const { dashboards } = state; const [selectedDashboard] = dashboards; const { data } = selectedDashboard; @@ -395,7 +295,6 @@ const dashboard = ( ...preWidget, { ...selectedWidget, - query, yAxisUnit, }, ...afterWidget, diff --git a/frontend/src/types/actions/dashboard.ts b/frontend/src/types/actions/dashboard.ts index edf3835c7b..a147eb7516 100644 --- a/frontend/src/types/actions/dashboard.ts +++ b/frontend/src/types/actions/dashboard.ts @@ -5,7 +5,6 @@ import { IDashboardVariable, Widgets, } from 'types/api/dashboard/getAll'; -import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { QueryData } from 'types/api/widgets/getQuery'; export const GET_DASHBOARD = 'GET_DASHBOARD'; @@ -30,9 +29,6 @@ export const DELETE_DASHBOARD_ERROR = 'DELETE_DASHBOARD_ERROR'; export const CREATE_DEFAULT_WIDGET = 'CREATE_DEFAULT_WIDGET'; -export const QUERY_SUCCESS = 'QUERY_SUCCESS'; -export const QUERY_ERROR = 'QUERY_ERROR'; - export const UPDATE_QUERY = 'UPDATE_QUERY'; export const APPLY_SETTINGS_TO_PANEL = 'APPLY_SETTINGS_TO_PANEL'; @@ -132,30 +128,15 @@ export interface QuerySuccessPayload { // query: string }; } -interface QuerySuccess { - type: typeof QUERY_SUCCESS; - payload: QuerySuccessPayload; -} interface UpdateQuery { type: typeof UPDATE_QUERY; payload: { - query: Query; widgetId: string; yAxisUnit: string | undefined; }; } -interface QueryError { - type: typeof QUERY_ERROR; - payload: { - errorMessage: string; - widgetId: string; - errorBoolean?: boolean; - isLoadingQueryResult?: boolean; - }; -} - interface SaveDashboardSuccess { type: typeof SAVE_SETTING_TO_PANEL_SUCCESS; payload: Dashboard; @@ -198,8 +179,6 @@ export type DashboardActions = | UpdateDashboardTitle | ToggleEditMode | CreateDefaultWidget - | QuerySuccess - | QueryError | ApplySettingsToPanel | SaveDashboardSuccess | WidgetDeleteSuccess diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index ce824db9a5..20e2a07702 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -3,8 +3,6 @@ import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems import { Layout } from 'react-grid-layout'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; -import { QueryData } from '../widgets/getQuery'; - export type PayloadProps = Dashboard[]; export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; @@ -65,16 +63,6 @@ export interface IBaseWidget { opacity: string; nullZeroValues: string; timePreferance: timePreferenceType; - queryData: { - loading: boolean; - error: boolean; - errorMessage: string; - data: { - query?: string; - legend?: string; - queryData: QueryData[]; - }; - }; stepSize?: number; yAxisUnit?: string; } diff --git a/frontend/src/types/api/dashboard/variables/query.ts b/frontend/src/types/api/dashboard/variables/query.ts index 4fe305600e..c535ad72be 100644 --- a/frontend/src/types/api/dashboard/variables/query.ts +++ b/frontend/src/types/api/dashboard/variables/query.ts @@ -1,6 +1,8 @@ +import { IDashboardVariable } from '../getAll'; + export type PayloadVariables = Record< string, - undefined | null | string | number | boolean | (string | number | boolean)[] + IDashboardVariable['selectedValue'] >; export type Props = { diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts index b89c81346a..ee9cfb6aef 100644 --- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts +++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts @@ -79,6 +79,7 @@ export interface Query { promql: IPromQLQuery[]; builder: QueryBuilderData; clickhouse_sql: IClickHouseQuery[]; + id: string; } export type QueryState = Omit; diff --git a/frontend/src/types/common/index.ts b/frontend/src/types/common/index.ts index c32bc6f44b..9e68e1173e 100644 --- a/frontend/src/types/common/index.ts +++ b/frontend/src/types/common/index.ts @@ -25,4 +25,8 @@ export type ErrorStatusCode = | BadRequest | Conflict; +export enum ErrorType { + NotFound = 'not_found', +} + export type StatusCode = SuccessStatusCode | ErrorStatusCode; diff --git a/frontend/src/types/common/queryBuilder.ts b/frontend/src/types/common/queryBuilder.ts index 0b2ded5ec5..16b6f8cd08 100644 --- a/frontend/src/types/common/queryBuilder.ts +++ b/frontend/src/types/common/queryBuilder.ts @@ -154,10 +154,9 @@ export type QueryBuilderData = { export type QueryBuilderContextType = { currentQuery: Query; + stagedQuery: Query | null; initialDataSource: DataSource | null; panelType: GRAPH_TYPES; - resetQueryBuilderData: () => void; - resetQueryBuilderInfo: () => void; handleSetQueryData: (index: number, queryData: IBuilderQuery) => void; handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void; handleSetQueryItemData: ( @@ -166,8 +165,6 @@ export type QueryBuilderContextType = { newQueryData: IPromQLQuery | IClickHouseQuery, ) => void; handleSetPanelType: (newPanelType: GRAPH_TYPES) => void; - handleSetQueryType: (newQueryType: EQueryType) => void; - initQueryBuilderData: (query: Partial) => void; setupInitialDataSource: (newInitialDataSource: DataSource | null) => void; removeQueryBuilderEntityByIndex: ( type: keyof QueryBuilderData, @@ -180,7 +177,12 @@ export type QueryBuilderContextType = { addNewBuilderQuery: () => void; addNewFormula: () => void; addNewQueryItem: (type: EQueryType.PROM | EQueryType.CLICKHOUSE) => void; - redirectWithQueryBuilderData: (query: Query) => void; + redirectWithQueryBuilderData: ( + query: Query, + searchParams?: Record, + ) => void; + handleRunQuery: () => void; + resetStagedQuery: () => void; }; export type QueryAdditionalFilter = { diff --git a/frontend/src/types/reducer/dashboards.ts b/frontend/src/types/reducer/dashboards.ts index 86628d7ffb..a8d5bb2bae 100644 --- a/frontend/src/types/reducer/dashboards.ts +++ b/frontend/src/types/reducer/dashboards.ts @@ -6,7 +6,5 @@ export default interface DashboardReducer { error: boolean; errorMessage: string; isEditMode: boolean; - isQueryFired: boolean; isAddWidget: boolean; - isLoadingQueryResult: boolean; } diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 3eb9032ba9..452d495fec 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -63,11 +63,13 @@ export const routePermission: Record = { SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'], SIGN_UP: ['ADMIN', 'EDITOR', 'VIEWER'], SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'], + TRACES_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], TRACE: ['ADMIN', 'EDITOR', 'VIEWER'], TRACE_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'], UN_AUTHORIZED: ['ADMIN', 'EDITOR', 'VIEWER'], USAGE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], VERSION: ['ADMIN', 'EDITOR', 'VIEWER'], LOGS: ['ADMIN', 'EDITOR', 'VIEWER'], + LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], LIST_LICENSES: ['ADMIN'], }; diff --git a/go.mod b/go.mod index ef31d473ed..753e8fd613 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.18 require ( github.com/ClickHouse/clickhouse-go/v2 v2.5.1 github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb + github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230523034029-2b7ff773052c + github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230517094211-cd3f3f0aea85 github.com/coreos/go-oidc/v3 v3.4.0 github.com/dustin/go-humanize v1.0.0 github.com/go-kit/log v0.2.1 @@ -33,9 +35,10 @@ require ( github.com/russellhaering/goxmldsig v1.2.0 github.com/samber/lo v1.38.1 github.com/sethvargo/go-password v0.2.0 - github.com/smartystreets/goconvey v1.6.4 + github.com/smartystreets/goconvey v1.8.0 github.com/soheilhy/cmux v0.1.5 go.opentelemetry.io/collector/confmap v0.70.0 + go.opentelemetry.io/otel/sdk v1.15.1 go.uber.org/zap v1.24.0 gopkg.in/segmentio/analytics-go.v3 v3.1.0 gopkg.in/yaml.v3 v3.0.1 @@ -50,7 +53,7 @@ require ( github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.6.1 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/swag v0.22.1 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect @@ -69,6 +72,7 @@ require ( require ( github.com/ClickHouse/ch-go v0.51.0 // indirect + github.com/SigNoz/zap_otlp v0.0.0-20230517094211-cd3f3f0aea85 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/armon/go-metrics v0.4.0 // indirect @@ -77,6 +81,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.1 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/cpuid v1.2.3 // indirect @@ -88,6 +93,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect go.opentelemetry.io/collector/featuregate v0.70.0 // indirect + go.opentelemetry.io/proto/otlp v0.19.0 // indirect + google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect ) @@ -104,9 +111,9 @@ require ( github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect + github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/gosimple/unidecode v1.0.0 // indirect github.com/hashicorp/consul v1.1.1-0.20180615161029-bed22a81e9fd // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -127,24 +134,24 @@ require ( github.com/segmentio/backo-go v1.0.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect - github.com/smartystreets/assertions v1.1.0 + github.com/smartystreets/assertions v1.13.1 github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stretchr/testify v1.8.2 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect - go.opentelemetry.io/otel v1.11.2 // indirect - go.opentelemetry.io/otel/trace v1.11.2 // indirect + go.opentelemetry.io/otel v1.15.1 + go.opentelemetry.io/otel/trace v1.15.1 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 golang.org/x/crypto v0.1.0 - golang.org/x/net v0.4.0 - golang.org/x/oauth2 v0.3.0 + golang.org/x/net v0.8.0 + golang.org/x/oauth2 v0.6.0 golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/grpc v1.51.0 - google.golang.org/protobuf v1.28.1 + google.golang.org/grpc v1.55.0 + google.golang.org/protobuf v1.30.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index d43d43782b..5e49f85e71 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,12 @@ github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb h1:bneLSKPf9YUSFm github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb/go.mod h1:JznGDNg9x1cujDKa22RaQOimOvvEfy3nxzDGd8XDgmA= github.com/SigNoz/prometheus v1.9.77-0.2 h1:y5HpJR6RYkOd5ysP9rSsLgoGMj0A7EvP5cbqp5XY0Mc= github.com/SigNoz/prometheus v1.9.77-0.2/go.mod h1:bT6BCBpZQA4qOO8oJPvcZr80XpbZcn7go6503fxpYj4= +github.com/SigNoz/zap_otlp v0.0.0-20230517094211-cd3f3f0aea85 h1:Q8yY/S8tetcuZF02XHXSYZzxn9n3voEF82XKJjvxJgk= +github.com/SigNoz/zap_otlp v0.0.0-20230517094211-cd3f3f0aea85/go.mod h1:crDWweGk4YMuJM58GNkasbV/Z2D37px3PS4DpUuTbYg= +github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230523034029-2b7ff773052c h1:jx+NF9RsxKZ24y/iBlPN6NIVjYBMlUeCTGsFLLna1Sw= +github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230523034029-2b7ff773052c/go.mod h1:HRisMAQR1ndayIyjklWlJy6xWCZU8EWMaZSJK4w+GrA= +github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230517094211-cd3f3f0aea85 h1:r8P7AbNkivf6CElyMEaxUFzwAkuJ0wbX2vMqPgLqRA8= +github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230517094211-cd3f3f0aea85/go.mod h1:e68FpYSwt1ujeEgKTFs+6c7BRmPxgI6mxm+P4+zBNLY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -143,7 +149,7 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc h1:PYXxkRUBGUMa5xgMVMDl62vEklZvKpVaxQeN9ie7Hfk= +github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195 h1:58f1tJ1ra+zFINPlwLWvQsR9CzAKt2e+EWV2yX9oXQ4= github.com/coreos/go-oidc/v3 v3.4.0 h1:xz7elHb/LDwm/ERpwHd+5nb7wFHL32rsr6bBOgaeu6g= github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -177,9 +183,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3 h1:xdCVXxEe0Y3FQith+0cj2irwZudqGYvecuLB1HtdexY= +github.com/envoyproxy/go-control-plane v0.11.0 h1:jtLewhRR2vMRNnq2ZZUoCjUlgut+Y0+sDDWPOfwOi1o= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.9.1 h1:PS7VIOgmSVhWUEeZwTe7z7zouA22Cr590PzXKbZHOVY= +github.com/envoyproxy/protoc-gen-validate v0.10.0 h1:oIfnZFdC0YhpNNEX+SuIqko4cqqVZeN9IGTrhZje83Y= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= @@ -215,8 +221,8 @@ github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNV github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= @@ -243,6 +249,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -271,8 +279,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -332,8 +341,9 @@ github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gophercloud/gophercloud v1.1.1 h1:MuGyqbSxiuVBqkPZ3+Nhbytk1xZxhmfCB2Rg1cJWFWM= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= @@ -349,6 +359,9 @@ github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.1 h1:/sDbPb60SusIXjiJGYLUoS/rAQurQmvGWmwn2bBPM9c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.1/go.mod h1:G+WkljZi4mflcqVxYSgvt8MNctRQHjEH8ubKtt1Ka3w= github.com/hashicorp/consul v1.1.1-0.20180615161029-bed22a81e9fd h1:u6o+bd6FHxDKoCSa8PJ5vrHhAYSKgJtAHQtLO1EYgos= github.com/hashicorp/consul v1.1.1-0.20180615161029-bed22a81e9fd/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= @@ -627,11 +640,13 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU= +github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w= +github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -682,13 +697,17 @@ go.opentelemetry.io/collector/featuregate v0.70.0 h1:Xr6hrMT/++SjTm06nreex8WlpgF go.opentelemetry.io/collector/featuregate v0.70.0/go.mod h1:ih+oCwrHW3bLac/qnPUzes28yDCDmh8WzsAKKauwCYI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.37.0 h1:yt2NKzK7Vyo6h0+X8BA4FpreZQTlVEIarnsBP/H5mzs= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.37.0/go.mod h1:+ARmXlUlc51J7sZeCBkBJNdHGySrdOzgzxp6VWRWM1U= -go.opentelemetry.io/otel v1.11.2 h1:YBZcQlsVekzFsFbjygXMOXSs6pialIZxcjfO/mBDmR0= -go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI= +go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8= +go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc= go.opentelemetry.io/otel/metric v0.34.0 h1:MCPoQxcg/26EuuJwpYN1mZTeCYAUGx8ABxfW07YkjP8= go.opentelemetry.io/otel/metric v0.34.0/go.mod h1:ZFuI4yQGNCupurTXCwkeD/zHBt+C2bR7bw5JqUm/AP8= -go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0= -go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA= +go.opentelemetry.io/otel/sdk v1.15.1 h1:5FKR+skgpzvhPQHIEfcwMYjCBr14LWzs3uSqKiQzETI= +go.opentelemetry.io/otel/sdk v1.15.1/go.mod h1:8rVtxQfrbmbHKfqzpQkT5EzZMcbMBwTzNAggbEAM0KA= +go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY= +go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -750,7 +769,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -806,8 +825,8 @@ golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -829,8 +848,8 @@ golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= -golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -933,12 +952,12 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -950,8 +969,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1014,7 +1033,7 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1148,7 +1167,8 @@ google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1177,13 +1197,14 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1199,8 +1220,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 50e54f40d7..fb59650982 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -4203,15 +4203,15 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([ var ( columnTypes = rows.ColumnTypes() columnNames = rows.Columns() - vars = make([]interface{}, len(columnTypes)) ) - for i := range columnTypes { - vars[i] = reflect.New(columnTypes[i].ScanType()).Interface() - } var rowList []*v3.Row for rows.Next() { + var vars = make([]interface{}, len(columnTypes)) + for i := range columnTypes { + vars[i] = reflect.New(columnTypes[i].ScanType()).Interface() + } if err := rows.Scan(vars...); err != nil { return nil, err } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index b1ecc25aad..4eb48d1064 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -2695,13 +2695,17 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que var queries map[string]string switch queryRangeParams.CompositeQuery.QueryType { case v3.QueryTypeBuilder: - // get the fields if any logs query is present - var fields map[string]v3.AttributeKey - fields, err = aH.getLogFieldsV3(ctx, queryRangeParams) - if err != nil { - apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err} - RespondError(w, apiErrObj, errQuriesByName) - return + // check if any enrichment is required for logs if yes then enrich them + if logsv3.EnrichmentRequired(queryRangeParams) { + // get the fields if any logs query is present + var fields map[string]v3.AttributeKey + fields, err = aH.getLogFieldsV3(ctx, queryRangeParams) + if err != nil { + apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err} + RespondError(w, apiErrObj, errQuriesByName) + return + } + logsv3.Enrich(queryRangeParams, fields) } var spanKeys map[string]v3.AttributeKey @@ -2712,7 +2716,7 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que return } - queries, err = aH.queryBuilder.PrepareQueries(queryRangeParams, fields, spanKeys) + queries, err = aH.queryBuilder.PrepareQueries(queryRangeParams, spanKeys) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return diff --git a/pkg/query-service/app/logs/parser.go b/pkg/query-service/app/logs/parser.go index 393c418b32..a661d4e894 100644 --- a/pkg/query-service/app/logs/parser.go +++ b/pkg/query-service/app/logs/parser.go @@ -36,8 +36,8 @@ const ( DESC = "desc" ) -var tokenRegex, _ = regexp.Compile(`(?i)(and( )*?|or( )*?)?(([\w.-]+( )+(in|nin)( )+\([^(]+\))|([\w.]+( )+(gt|lt|gte|lte)( )+(')?[\S]+(')?)|([\w.]+( )+(contains|ncontains))( )+[^\\]?'(.*?[^\\])')`) -var operatorRegex, _ = regexp.Compile(`(?i)(?: )(in|nin|gt|lt|gte|lte|contains|ncontains)(?: )`) +var tokenRegex, _ = regexp.Compile(`(?i)(and( )*?|or( )*?)?(([\w.-]+( )+(in|nin)( )+\([^(]+\))|([\w.]+( )+(gt|lt|gte|lte)( )+(')?[\S]+(')?)|([\w.]+( )+(contains|ncontains))( )+[^\\]?'(.*?[^\\])'|([\w.]+( )+(exists|nexists)( )?))`) +var operatorRegex, _ = regexp.Compile(`(?i)(?: )(in|nin|gte|lte|gt|lt|contains|ncontains|exists|nexists)(?: )?`) func ParseLogFilterParams(r *http.Request) (*model.LogsFilterParams, error) { res := model.LogsFilterParams{ @@ -191,13 +191,18 @@ func parseLogQuery(query string) ([]string, error) { sqlQueryTokens = append(sqlQueryTokens, f) } else { symbol := operatorMapping[strings.ToLower(op)] - sqlExpr := strings.Replace(v, " "+op+" ", " "+symbol+" ", 1) - splittedExpr := strings.Split(sqlExpr, symbol) - if len(splittedExpr) != 2 { - return nil, fmt.Errorf("error while splitting expression: %s", sqlExpr) + if symbol != "" { + sqlExpr := strings.Replace(v, " "+op+" ", " "+symbol+" ", 1) + splittedExpr := strings.Split(sqlExpr, symbol) + if len(splittedExpr) != 2 { + return nil, fmt.Errorf("error while splitting expression: %s", sqlExpr) + } + trimmedSqlExpr := fmt.Sprintf("%s %s %s ", strings.Join(strings.Fields(splittedExpr[0]), " "), symbol, strings.TrimSpace(splittedExpr[1])) + sqlQueryTokens = append(sqlQueryTokens, trimmedSqlExpr) + } else { + // for exists|nexists don't process it here since we don't have metadata + sqlQueryTokens = append(sqlQueryTokens, v) } - trimmedSqlExpr := fmt.Sprintf("%s %s %s ", strings.Join(strings.Fields(splittedExpr[0]), " "), symbol, strings.TrimSpace(splittedExpr[1])) - sqlQueryTokens = append(sqlQueryTokens, trimmedSqlExpr) } } @@ -209,9 +214,6 @@ func parseColumn(s string) (*string, error) { // if has and/or as prefix filter := strings.Split(s, " ") - if len(filter) < 3 { - return nil, fmt.Errorf("incorrect filter") - } first := strings.ToLower(filter[0]) if first == AND || first == OR { @@ -247,6 +249,9 @@ func replaceInterestingFields(allFields *model.GetFieldsResponse, queryTokens [] } func replaceFieldInToken(queryToken string, selectedFieldsLookup map[string]model.LogField, interestingFieldLookup map[string]model.LogField) (string, error) { + op := strings.TrimSpace(operatorRegex.FindString(queryToken)) + opLower := strings.ToLower(op) + col, err := parseColumn(queryToken) if err != nil { return "", err @@ -254,6 +259,42 @@ func replaceFieldInToken(queryToken string, selectedFieldsLookup map[string]mode sqlColName := *col lowerColName := strings.ToLower(*col) + + if opLower == "exists" || opLower == "nexists" { + var result string + + // handle static fields which are columns, timestamp and id is not required but added them regardless + defaultValue := "" + if lowerColName == "trace_id" || lowerColName == "span_id" || lowerColName == "severity_text" || lowerColName == "id" { + defaultValue = "''" + } + if lowerColName == "trace_flags" || lowerColName == "severity_number" || lowerColName == "timestamp" { + defaultValue = "0" + } + + if defaultValue != "" { + if opLower == "exists" { + result = fmt.Sprintf("%s != %s", sqlColName, defaultValue) + } else { + result = fmt.Sprintf("%s = %s", sqlColName, defaultValue) + } + } else { + // creating the query token here as we have the metadata + field := model.LogField{} + + if sfield, ok := selectedFieldsLookup[sqlColName]; ok { + field = sfield + } else if ifield, ok := interestingFieldLookup[sqlColName]; ok { + field = ifield + } + result = fmt.Sprintf("has(%s_%s_key, '%s')", field.Type, strings.ToLower(field.DataType), field.Name) + if opLower == "nexists" { + result = "NOT " + result + } + } + return strings.Replace(queryToken, sqlColName+" "+op, result, 1), nil + } + if lowerColName != "body" { if _, ok := selectedFieldsLookup[sqlColName]; !ok { if field, ok := interestingFieldLookup[sqlColName]; ok { diff --git a/pkg/query-service/app/logs/parser_test.go b/pkg/query-service/app/logs/parser_test.go index 843de2725c..8bb70991d6 100644 --- a/pkg/query-service/app/logs/parser_test.go +++ b/pkg/query-service/app/logs/parser_test.go @@ -102,6 +102,11 @@ var correctQueriesTest = []struct { `userIdentifier in ('user') and userIdentifier contains 'user'`, []string{`userIdentifier IN ('user') `, `AND userIdentifier ILIKE '%user%' `}, }, + { + `filters with for exists`, + `userIdentifier exists and user nexists`, + []string{`userIdentifier exists `, `and user nexists`}, + }, } func TestParseLogQueryCorrect(t *testing.T) { @@ -206,6 +211,16 @@ var parseCorrectColumns = []struct { `AND body ILIKE '%searchstring%' `, "body", }, + { + "column with exists", + `AND user exists`, + "user", + }, + { + "column with nexists", + `AND user nexists `, + "user", + }, } func TestParseColumn(t *testing.T) { @@ -374,6 +389,24 @@ var generateSQLQueryTestCases = []struct { }, SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( field1 < 100 and attributes_int64_value[indexOf(attributes_int64_key, 'FielD1')] > 50 and Field2 > 10 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ", }, + { + Name: "Check exists and not exists", + Filter: model.LogsFilterParams{ + Query: "field1 exists and Field2 nexists and Field2 gt 10", + TimestampStart: uint64(1657689292000), + TimestampEnd: uint64(1657689294000), + }, + SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( has(attributes_int64_key, 'field1') and NOT has(attributes_double64_key, 'Field2') and Field2 > 10 ) ", + }, + { + Name: "Check exists and not exists on top level keys", + Filter: model.LogsFilterParams{ + Query: "trace_id exists and span_id nexists and trace_flags exists and severity_number nexists", + TimestampStart: uint64(1657689292000), + TimestampEnd: uint64(1657689294000), + }, + SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( trace_id != '' and span_id = '' and trace_flags != 0 and severity_number = 0) ", + }, } func TestGenerateSQLQuery(t *testing.T) { diff --git a/pkg/query-service/app/logs/v3/enrich_query.go b/pkg/query-service/app/logs/v3/enrich_query.go new file mode 100644 index 0000000000..88f9097814 --- /dev/null +++ b/pkg/query-service/app/logs/v3/enrich_query.go @@ -0,0 +1,145 @@ +package v3 + +import ( + "go.signoz.io/signoz/pkg/query-service/constants" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" +) + +func EnrichmentRequired(params *v3.QueryRangeParamsV3) bool { + compositeQuery := params.CompositeQuery + if compositeQuery == nil { + return false + } + + // Build queries for each builder query + for queryName, query := range compositeQuery.BuilderQueries { + if query.Expression != queryName && query.DataSource != v3.DataSourceLogs { + continue + } + + // check aggregation attribute + if query.AggregateAttribute.Key != "" { + if !isEnriched(query.AggregateAttribute) { + return true + } + } + + // check filter attribute + if query.Filters != nil && len(query.Filters.Items) != 0 { + for _, item := range query.Filters.Items { + if !isEnriched(item.Key) { + return true + } + } + } + + groupByLookup := map[string]struct{}{} + // check groupby + for _, groupBy := range query.GroupBy { + if !isEnriched(groupBy) { + return true + } + groupByLookup[groupBy.Key] = struct{}{} + } + + // check orderby + for _, orderBy := range query.OrderBy { + if _, ok := groupByLookup[orderBy.ColumnName]; !ok { + key := v3.AttributeKey{Key: orderBy.ColumnName} + if !isEnriched(key) { + return true + } + } + } + + } + + return false +} + +func isEnriched(field v3.AttributeKey) bool { + // if it is timestamp/id dont check + if field.Key == "timestamp" || field.Key == "id" || field.Key == constants.SigNozOrderByValue { + return true + } + + if field.IsColumn { + return true + } + + if field.Type == v3.AttributeKeyTypeUnspecified || field.DataType == v3.AttributeKeyDataTypeUnspecified { + return false + } + return true +} + +func Enrich(params *v3.QueryRangeParamsV3, fields map[string]v3.AttributeKey) { + compositeQuery := params.CompositeQuery + if compositeQuery == nil { + return + } + + // Build queries for each builder query + for queryName, query := range compositeQuery.BuilderQueries { + if query.Expression != queryName && query.DataSource != v3.DataSourceLogs { + continue + } + enrichLogsQuery(query, fields) + } +} + +func enrichLogsQuery(query *v3.BuilderQuery, fields map[string]v3.AttributeKey) error { + // enrich aggregation attribute + if query.AggregateAttribute.Key != "" { + query.AggregateAttribute = enrichFieldWithMetadata(query.AggregateAttribute, fields) + } + + // enrich filter attribute + if query.Filters != nil && len(query.Filters.Items) != 0 { + for i := 0; i < len(query.Filters.Items); i++ { + query.Filters.Items[i].Key = enrichFieldWithMetadata(query.Filters.Items[i].Key, fields) + } + } + + // enrich groupby + for i := 0; i < len(query.GroupBy); i++ { + query.GroupBy[i] = enrichFieldWithMetadata(query.GroupBy[i], fields) + } + + // enrich orderby + for i := 0; i < len(query.OrderBy); i++ { + key := v3.AttributeKey{Key: query.OrderBy[i].ColumnName} + key = enrichFieldWithMetadata(key, fields) + query.OrderBy[i].Key = key.Key + query.OrderBy[i].Type = key.Type + query.OrderBy[i].DataType = key.DataType + query.OrderBy[i].IsColumn = key.IsColumn + } + return nil +} + +func enrichFieldWithMetadata(field v3.AttributeKey, fields map[string]v3.AttributeKey) v3.AttributeKey { + if isEnriched(field) { + return field + } + + // if type is unknown check if it is a top level key + if v, ok := constants.StaticFieldsLogsV3[field.Key]; ok { + return v + } + + // check if the field is present in the fields map + if existingField, ok := fields[field.Key]; ok { + if existingField.IsColumn { + return field + } + field.Type = existingField.Type + field.DataType = existingField.DataType + return field + } + + // enrich with default values if metadata is not found + field.Type = v3.AttributeKeyTypeTag + field.DataType = v3.AttributeKeyDataTypeString + return field +} diff --git a/pkg/query-service/app/logs/v3/enrich_query_test.go b/pkg/query-service/app/logs/v3/enrich_query_test.go new file mode 100644 index 0000000000..16d5e74404 --- /dev/null +++ b/pkg/query-service/app/logs/v3/enrich_query_test.go @@ -0,0 +1,273 @@ +package v3 + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" +) + +var testEnrichmentRequiredData = []struct { + Name string + Params v3.QueryRangeParamsV3 + EnrichmentRequired bool +}{ + { + Name: "attribute enrichment not required", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + AggregateAttribute: v3.AttributeKey{ + Key: "test", + Type: v3.AttributeKeyTypeTag, + DataType: v3.AttributeKeyDataTypeInt64, + }, + }, + }, + }, + }, + EnrichmentRequired: false, + }, + { + Name: "attribute enrichment required", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + AggregateAttribute: v3.AttributeKey{ + Key: "test", + }, + }, + }, + }, + }, + EnrichmentRequired: true, + }, + { + Name: "filter enrichment not required", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ + {Key: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "john", Operator: "="}, + }}, + }, + }, + }, + }, + EnrichmentRequired: false, + }, + { + Name: "filter enrichment required", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ + {Key: v3.AttributeKey{Key: "user_name"}, Value: "john", Operator: "="}, + }}, + }, + }, + }, + }, + EnrichmentRequired: true, + }, + { + Name: "groupBy enrichment not required", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + GroupBy: []v3.AttributeKey{{Key: "userid", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, + }, + }, + }, + }, + EnrichmentRequired: false, + }, + { + Name: "groupBy enrichment required", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + GroupBy: []v3.AttributeKey{{Key: "userid"}}, + }, + }, + }, + }, + EnrichmentRequired: true, + }, + { + Name: "orderBy enrichment not required", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + GroupBy: []v3.AttributeKey{{Key: "userid", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, + OrderBy: []v3.OrderBy{{ColumnName: "userid"}}, + }, + }, + }, + }, + EnrichmentRequired: false, + }, + { + Name: "orderBy enrichment required", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + OrderBy: []v3.OrderBy{{ColumnName: "userid"}}, + }, + }, + }, + }, + EnrichmentRequired: true, + }, + { + Name: "top level key", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + GroupBy: []v3.AttributeKey{{Key: "trace_id", Type: v3.AttributeKeyTypeUnspecified, DataType: v3.AttributeKeyDataTypeString, IsColumn: true}}, + }, + }, + }, + }, + EnrichmentRequired: false, + }, + { + Name: "orderBy enrichment required", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + GroupBy: []v3.AttributeKey{{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, + OrderBy: []v3.OrderBy{{ColumnName: "#SIGNOZ_VALUE", Order: "ASC"}}, + }, + }, + }, + }, + EnrichmentRequired: false, + }, +} + +func TestEnrichmentRquired(t *testing.T) { + for _, tt := range testEnrichmentRequiredData { + Convey("testEnrichmentRequiredData", t, func() { + res := EnrichmentRequired(&tt.Params) + So(res, ShouldEqual, tt.EnrichmentRequired) + }) + } +} + +var testEnrichParamsData = []struct { + Name string + Params v3.QueryRangeParamsV3 + Fields map[string]v3.AttributeKey + Result v3.QueryRangeParamsV3 +}{ + { + Name: "Enriching query range v3 params", + Params: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + AggregateAttribute: v3.AttributeKey{ + Key: "test", + }, + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ + {Key: v3.AttributeKey{Key: "user_name"}, Value: "john", Operator: "="}, + }}, + GroupBy: []v3.AttributeKey{{Key: "trace_id"}}, + OrderBy: []v3.OrderBy{{ColumnName: "response_time"}}, + }, + }, + }, + }, + Fields: map[string]v3.AttributeKey{ + "test": { + Key: "test", + Type: v3.AttributeKeyTypeTag, + DataType: v3.AttributeKeyDataTypeInt64, + }, + "user_name": { + Key: "user_name", + Type: v3.AttributeKeyTypeTag, + DataType: v3.AttributeKeyDataTypeString, + }, + "response_time": { + Key: "response_time", + Type: v3.AttributeKeyTypeTag, + DataType: v3.AttributeKeyDataTypeInt64, + }, + }, + Result: v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "test": { + QueryName: "test", + Expression: "test", + DataSource: v3.DataSourceLogs, + AggregateAttribute: v3.AttributeKey{ + Key: "test", + Type: v3.AttributeKeyTypeTag, + DataType: v3.AttributeKeyDataTypeInt64, + }, + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ + {Key: v3.AttributeKey{Key: "user_name", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeString}, Value: "john", Operator: "="}, + }}, + GroupBy: []v3.AttributeKey{{Key: "trace_id", Type: v3.AttributeKeyTypeUnspecified, DataType: v3.AttributeKeyDataTypeString, IsColumn: true}}, + OrderBy: []v3.OrderBy{{ColumnName: "response_time", Key: "response_time", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeInt64}}, + }, + }, + }, + }, + }, +} + +func TestEnrichParams(t *testing.T) { + for _, tt := range testEnrichParamsData { + Convey("testEnrichmentRequiredData", t, func() { + Enrich(&tt.Params, tt.Fields) + So(tt.Params, ShouldResemble, tt.Result) + }) + } +} diff --git a/pkg/query-service/app/logs/v3/query_builder.go b/pkg/query-service/app/logs/v3/query_builder.go index ba7b3438b0..6ff58a2497 100644 --- a/pkg/query-service/app/logs/v3/query_builder.go +++ b/pkg/query-service/app/logs/v3/query_builder.go @@ -53,33 +53,6 @@ var logOperators = map[v3.FilterOperator]string{ // (todo) check contains/not contains/ } -func enrichFieldWithMetadata(field v3.AttributeKey, fields map[string]v3.AttributeKey) v3.AttributeKey { - if field.Type == "" || field.DataType == "" { - // if type is unknown check if it is a top level key - if v, ok := constants.StaticFieldsLogsV3[field.Key]; ok { - if (v3.AttributeKey{} != v) { - return v - } - } - - // check if the field is present in the fields map - if existingField, ok := fields[field.Key]; ok { - if existingField.IsColumn { - return field - } - field.Type = existingField.Type - field.DataType = existingField.DataType - return field - } - - // enrich with default values if metadata is not found - field.Type = v3.AttributeKeyTypeTag - field.DataType = v3.AttributeKeyDataTypeString - - } - return field -} - func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string { if columnType == v3.AttributeKeyTypeTag { return "attributes" @@ -99,8 +72,12 @@ func getClickhouseLogsColumnDataType(columnDataType v3.AttributeKeyDataType) str } // getClickhouseColumnName returns the corresponding clickhouse column name for the given attribute/resource key -func getClickhouseColumnName(key v3.AttributeKey, fields map[string]v3.AttributeKey) (string, error) { +func getClickhouseColumnName(key v3.AttributeKey) string { clickhouseColumn := key.Key + if key.Key == constants.TIMESTAMP || key.Key == "id" { + return key.Key + } + //if the key is present in the topLevelColumn then it will be only searched in those columns, //regardless if it is indexed/present again in resource or column attribute if !key.IsColumn { @@ -108,56 +85,44 @@ func getClickhouseColumnName(key v3.AttributeKey, fields map[string]v3.Attribute columnDataType := getClickhouseLogsColumnDataType(key.DataType) clickhouseColumn = fmt.Sprintf("%s_%s_value[indexOf(%s_%s_key, '%s')]", columnType, columnDataType, columnType, columnDataType, key.Key) } - return clickhouseColumn, nil + return clickhouseColumn } // getSelectLabels returns the select labels for the query based on groupBy and aggregateOperator -func getSelectLabels(aggregatorOperator v3.AggregateOperator, groupBy []v3.AttributeKey, fields map[string]v3.AttributeKey) (string, error) { +func getSelectLabels(aggregatorOperator v3.AggregateOperator, groupBy []v3.AttributeKey) (string, error) { var selectLabels string if aggregatorOperator == v3.AggregateOperatorNoOp { selectLabels = "" } else { for _, tag := range groupBy { - enrichedTag := enrichFieldWithMetadata(tag, fields) - columnName, err := getClickhouseColumnName(enrichedTag, fields) - if err != nil { - return "", err - } + columnName := getClickhouseColumnName(tag) selectLabels += fmt.Sprintf(", %s as %s", columnName, tag.Key) } } return selectLabels, nil } -func buildLogsTimeSeriesFilterQuery(fs *v3.FilterSet, groupBy []v3.AttributeKey, fields map[string]v3.AttributeKey) (string, error) { +func buildLogsTimeSeriesFilterQuery(fs *v3.FilterSet, groupBy []v3.AttributeKey) (string, error) { var conditions []string if fs != nil && len(fs.Items) != 0 { for _, item := range fs.Items { op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator)))) - key := enrichFieldWithMetadata(item.Key, fields) - value, err := utils.ValidateAndCastValue(item.Value, key.DataType) + value, err := utils.ValidateAndCastValue(item.Value, item.Key.DataType) if err != nil { return "", fmt.Errorf("failed to validate and cast value for %s: %v", item.Key.Key, err) } if logsOp, ok := logOperators[op]; ok { switch op { case v3.FilterOperatorExists, v3.FilterOperatorNotExists: - columnType := getClickhouseLogsColumnType(key.Type) - columnDataType := getClickhouseLogsColumnDataType(key.DataType) - conditions = append(conditions, fmt.Sprintf(logsOp, columnType, columnDataType, key.Key)) + columnType := getClickhouseLogsColumnType(item.Key.Type) + columnDataType := getClickhouseLogsColumnDataType(item.Key.DataType) + conditions = append(conditions, fmt.Sprintf(logsOp, columnType, columnDataType, item.Key.Key)) case v3.FilterOperatorContains, v3.FilterOperatorNotContains: - columnName, err := getClickhouseColumnName(key, fields) - if err != nil { - return "", err - } + columnName := getClickhouseColumnName(item.Key) conditions = append(conditions, fmt.Sprintf("%s %s '%%%s%%'", columnName, logsOp, item.Value)) default: - columnName, err := getClickhouseColumnName(key, fields) - if err != nil { - return "", err - } - + columnName := getClickhouseColumnName(item.Key) fmtVal := utils.ClickHouseFormattedValue(value) conditions = append(conditions, fmt.Sprintf("%s %s %s", columnName, logsOp, fmtVal)) } @@ -169,11 +134,10 @@ func buildLogsTimeSeriesFilterQuery(fs *v3.FilterSet, groupBy []v3.AttributeKey, // add group by conditions to filter out log lines which doesn't have the key for _, attr := range groupBy { - enrichedAttr := enrichFieldWithMetadata(attr, fields) - if !enrichedAttr.IsColumn { - columnType := getClickhouseLogsColumnType(enrichedAttr.Type) - columnDataType := getClickhouseLogsColumnDataType(enrichedAttr.DataType) - conditions = append(conditions, fmt.Sprintf("indexOf(%s_%s_key, '%s') > 0", columnType, columnDataType, enrichedAttr.Key)) + if !attr.IsColumn { + columnType := getClickhouseLogsColumnType(attr.Type) + columnDataType := getClickhouseLogsColumnDataType(attr.DataType) + conditions = append(conditions, fmt.Sprintf("indexOf(%s_%s_key, '%s') > 0", columnType, columnDataType, attr.Key)) } } @@ -199,9 +163,9 @@ func getZerosForEpochNano(epoch int64) int64 { return int64(math.Pow(10, float64(19-count))) } -func buildLogsQuery(start, end, step int64, mq *v3.BuilderQuery, fields map[string]v3.AttributeKey) (string, error) { +func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.BuilderQuery) (string, error) { - filterSubQuery, err := buildLogsTimeSeriesFilterQuery(mq.Filters, mq.GroupBy, fields) + filterSubQuery, err := buildLogsTimeSeriesFilterQuery(mq.Filters, mq.GroupBy) if err != nil { return "", err } @@ -209,7 +173,7 @@ func buildLogsQuery(start, end, step int64, mq *v3.BuilderQuery, fields map[stri // timerange will be sent in epoch millisecond timeFilter := fmt.Sprintf("(timestamp >= %d AND timestamp <= %d)", start*getZerosForEpochNano(start), end*getZerosForEpochNano(end)) - selectLabels, err := getSelectLabels(mq.AggregateOperator, mq.GroupBy, fields) + selectLabels, err := getSelectLabels(mq.AggregateOperator, mq.GroupBy) if err != nil { return "", err } @@ -225,18 +189,14 @@ func buildLogsQuery(start, end, step int64, mq *v3.BuilderQuery, fields map[stri "from signoz_logs.distributed_logs " + "where " + timeFilter + "%s " + "group by %s%s " + - "order by %sts" + "order by %s" groupBy := groupByAttributeKeyTags(mq.GroupBy...) - orderBy := orderByAttributeKeyTags(mq.OrderBy, mq.GroupBy) + orderBy := orderByAttributeKeyTags(panelType, mq.AggregateOperator, mq.OrderBy, mq.GroupBy) aggregationKey := "" if mq.AggregateAttribute.Key != "" { - enrichedAttribute := enrichFieldWithMetadata(mq.AggregateAttribute, fields) - aggregationKey, err = getClickhouseColumnName(enrichedAttribute, fields) - if err != nil { - return "", err - } + aggregationKey = getClickhouseColumnName(mq.AggregateAttribute) } switch mq.AggregateOperator { @@ -271,9 +231,8 @@ func buildLogsQuery(start, end, step int64, mq *v3.BuilderQuery, fields map[stri return query, nil case v3.AggregateOperatorCount: if mq.AggregateAttribute.Key != "" { - field := enrichFieldWithMetadata(mq.AggregateAttribute, fields) - columnType := getClickhouseLogsColumnType(field.Type) - columnDataType := getClickhouseLogsColumnDataType(field.DataType) + columnType := getClickhouseLogsColumnType(mq.AggregateAttribute.Type) + columnDataType := getClickhouseLogsColumnDataType(mq.AggregateAttribute.DataType) filterSubQuery = fmt.Sprintf("%s AND has(%s_%s_key, '%s')", filterSubQuery, columnType, columnDataType, mq.AggregateAttribute.Key) } @@ -285,8 +244,8 @@ func buildLogsQuery(start, end, step int64, mq *v3.BuilderQuery, fields map[stri query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorNoOp: - queryTmpl := constants.LogsSQLSelect + "from signoz_logs.distributed_logs where %s %s" - query := fmt.Sprintf(queryTmpl, timeFilter, filterSubQuery) + queryTmpl := constants.LogsSQLSelect + "from signoz_logs.distributed_logs where %s %sorder by %s" + query := fmt.Sprintf(queryTmpl, timeFilter, filterSubQuery, orderBy) return query, nil default: return "", fmt.Errorf("unsupported aggregate operator") @@ -309,19 +268,25 @@ func groupByAttributeKeyTags(tags ...v3.AttributeKey) string { } // orderBy returns a string of comma separated tags for order by clause +// if there are remaining items which are not present in tags they are also added // if the order is not specified, it defaults to ASC -func orderBy(items []v3.OrderBy, tags []string) string { +func orderBy(panelType v3.PanelType, items []v3.OrderBy, tags []string) []string { var orderBy []string + + // create a lookup + addedToOrderBy := map[string]bool{} + itemsLookup := map[string]v3.OrderBy{} + + for i := 0; i < len(items); i++ { + addedToOrderBy[items[i].ColumnName] = false + itemsLookup[items[i].ColumnName] = items[i] + } + for _, tag := range tags { - found := false - for _, item := range items { - if item.ColumnName == tag { - found = true - orderBy = append(orderBy, fmt.Sprintf("%s %s", item.ColumnName, item.Order)) - break - } - } - if !found { + if item, ok := itemsLookup[tag]; ok { + orderBy = append(orderBy, fmt.Sprintf("%s %s", item.ColumnName, item.Order)) + addedToOrderBy[item.ColumnName] = true + } else { orderBy = append(orderBy, fmt.Sprintf("%s ASC", tag)) } } @@ -330,20 +295,48 @@ func orderBy(items []v3.OrderBy, tags []string) string { for _, item := range items { if item.ColumnName == constants.SigNozOrderByValue { orderBy = append(orderBy, fmt.Sprintf("value %s", item.Order)) + addedToOrderBy[item.ColumnName] = true } } - return strings.Join(orderBy, ",") + + // add the remaining items + if panelType == v3.PanelTypeList { + for _, item := range items { + // since these are not present in tags we will have to select them correctly + // for list view there is no need to check if it was added since they wont be added yet but this is just for safety + if !addedToOrderBy[item.ColumnName] { + attr := v3.AttributeKey{Key: item.ColumnName, DataType: item.DataType, Type: item.Type, IsColumn: item.IsColumn} + name := getClickhouseColumnName(attr) + orderBy = append(orderBy, fmt.Sprintf("%s %s", name, item.Order)) + } + } + } + return orderBy } -func orderByAttributeKeyTags(items []v3.OrderBy, tags []v3.AttributeKey) string { +func orderByAttributeKeyTags(panelType v3.PanelType, aggregatorOperator v3.AggregateOperator, items []v3.OrderBy, tags []v3.AttributeKey) string { var groupTags []string for _, tag := range tags { groupTags = append(groupTags, tag.Key) } - str := orderBy(items, groupTags) - if len(str) > 0 { - str = str + "," + orderByArray := orderBy(panelType, items, groupTags) + + found := false + for i := 0; i < len(orderByArray); i++ { + if strings.Compare(orderByArray[i], constants.TIMESTAMP) == 0 { + orderByArray[i] = "ts" + break + } } + if !found { + if aggregatorOperator == v3.AggregateOperatorNoOp { + orderByArray = append(orderByArray, constants.TIMESTAMP) + } else { + orderByArray = append(orderByArray, "ts") + } + } + + str := strings.Join(orderByArray, ",") return str } @@ -376,22 +369,16 @@ func reduceQuery(query string, reduceTo v3.ReduceToOperator, aggregateOperator v return query, nil } -func addLimitToQuery(query string, limit uint64, panelType v3.PanelType) string { - if limit == 0 { - limit = 100 - } - if panelType == v3.PanelTypeList { - return fmt.Sprintf("%s LIMIT %d", query, limit) - } - return query +func addLimitToQuery(query string, limit uint64) string { + return fmt.Sprintf("%s LIMIT %d", query, limit) } func addOffsetToQuery(query string, offset uint64) string { return fmt.Sprintf("%s OFFSET %d", query, offset) } -func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, fields map[string]v3.AttributeKey) (string, error) { - query, err := buildLogsQuery(start, end, mq.StepInterval, mq, fields) +func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery) (string, error) { + query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq) if err != nil { return "", err } @@ -399,10 +386,16 @@ func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.Pan query, err = reduceQuery(query, mq.ReduceTo, mq.AggregateOperator) } - query = addLimitToQuery(query, mq.Limit, panelType) - - if mq.Offset != 0 { - query = addOffsetToQuery(query, mq.Offset) + if panelType == v3.PanelTypeList { + if mq.PageSize > 0 { + if mq.Limit > 0 && mq.Offset > mq.Limit { + return "", fmt.Errorf("max limit exceeded") + } + query = addLimitToQuery(query, mq.PageSize) + query = addOffsetToQuery(query, mq.Offset) + } else { + query = addLimitToQuery(query, mq.Limit) + } } return query, err diff --git a/pkg/query-service/app/logs/v3/query_builder_test.go b/pkg/query-service/app/logs/v3/query_builder_test.go index 11b2c20770..5103c7a177 100644 --- a/pkg/query-service/app/logs/v3/query_builder_test.go +++ b/pkg/query-service/app/logs/v3/query_builder_test.go @@ -43,8 +43,7 @@ var testGetClickhouseColumnNameData = []struct { func TestGetClickhouseColumnName(t *testing.T) { for _, tt := range testGetClickhouseColumnNameData { Convey("testGetClickhouseColumnNameData", t, func() { - columnName, err := getClickhouseColumnName(tt.AttributeKey, map[string]v3.AttributeKey{}) - So(err, ShouldBeNil) + columnName := getClickhouseColumnName(tt.AttributeKey) So(columnName, ShouldEqual, tt.ExpectedColumnName) }) } @@ -83,12 +82,6 @@ var testGetSelectLabelsData = []struct { GroupByTags: []v3.AttributeKey{{Key: "host", IsColumn: true}}, SelectLabels: ", host as host", }, - { - Name: "trace_id field with missing meta", - AggregateOperator: v3.AggregateOperatorCount, - GroupByTags: []v3.AttributeKey{{Key: "trace_id"}}, - SelectLabels: ", trace_id as trace_id", - }, { Name: "trace_id field as an attribute", AggregateOperator: v3.AggregateOperatorCount, @@ -100,7 +93,7 @@ var testGetSelectLabelsData = []struct { func TestGetSelectLabels(t *testing.T) { for _, tt := range testGetSelectLabelsData { Convey("testGetSelectLabelsData", t, func() { - selectLabels, err := getSelectLabels(tt.AggregateOperator, tt.GroupByTags, map[string]v3.AttributeKey{}) + selectLabels, err := getSelectLabels(tt.AggregateOperator, tt.GroupByTags) So(err, ShouldBeNil) So(selectLabels, ShouldEqual, tt.SelectLabels) }) @@ -187,20 +180,6 @@ var timeSeriesFilterQueryData = []struct { }}, ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'host')] NOT ILIKE '%102.%'", }, - { - Name: "Test no metadata", - FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ - {Key: v3.AttributeKey{Key: "host"}, Value: "102.", Operator: "ncontains"}, - }}, - ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'host')] NOT ILIKE '%102.%'", - }, - { - Name: "Test no metadata number", - FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ - {Key: v3.AttributeKey{Key: "bytes"}, Value: 102, Operator: "="}, - }}, - ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'bytes')] = '102'", - }, { Name: "Test groupBy", FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ @@ -220,32 +199,15 @@ var timeSeriesFilterQueryData = []struct { { Name: "Wrong data", FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ - {Key: v3.AttributeKey{Key: "bytes"}, Value: true, Operator: "="}, + {Key: v3.AttributeKey{Key: "bytes", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeFloat64}, Value: true, Operator: "="}, }}, - Fields: map[string]v3.AttributeKey{"bytes": {Key: "bytes", DataType: v3.AttributeKeyDataTypeFloat64, Type: v3.AttributeKeyTypeTag}}, - Error: "failed to validate and cast value for bytes: invalid data type, expected float, got bool", - }, - { - Name: "Cast data", - FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ - {Key: v3.AttributeKey{Key: "bytes"}, Value: 102, Operator: "="}, - }}, - Fields: map[string]v3.AttributeKey{"bytes": {Key: "bytes", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}}, - ExpectedFilter: " AND attributes_int64_value[indexOf(attributes_int64_key, 'bytes')] = 102", - }, - { - Name: "Test top level field w/o metadata", - FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ - {Key: v3.AttributeKey{Key: "body"}, Value: "%test%", Operator: "like"}, - }}, - ExpectedFilter: " AND body ILIKE '%test%'", + Error: "failed to validate and cast value for bytes: invalid data type, expected float, got bool", }, { Name: "Test top level field with metadata", FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ {Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "%test%", Operator: "like"}, }}, - Fields: map[string]v3.AttributeKey{"body": {Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'body')] ILIKE '%test%'", }, } @@ -253,7 +215,7 @@ var timeSeriesFilterQueryData = []struct { func TestBuildLogsTimeSeriesFilterQuery(t *testing.T) { for _, tt := range timeSeriesFilterQueryData { Convey("TestBuildLogsTimeSeriesFilterQuery", t, func() { - query, err := buildLogsTimeSeriesFilterQuery(tt.FilterSet, tt.GroupBy, tt.Fields) + query, err := buildLogsTimeSeriesFilterQuery(tt.FilterSet, tt.GroupBy) if tt.Error != "" { So(err.Error(), ShouldEqual, tt.Error) } else { @@ -267,6 +229,7 @@ func TestBuildLogsTimeSeriesFilterQuery(t *testing.T) { var testBuildLogsQueryData = []struct { Name string + PanelType v3.PanelType Start int64 End int64 Step int64 @@ -277,10 +240,11 @@ var testBuildLogsQueryData = []struct { ExpectedQuery string }{ { - Name: "Test aggregate count on select field", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate count on select field", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateOperator: v3.AggregateOperatorCount, @@ -290,10 +254,11 @@ var testBuildLogsQueryData = []struct { ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) group by ts order by ts", }, { - Name: "Test aggregate count on a attribute", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate count on a attribute", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, @@ -304,10 +269,11 @@ var testBuildLogsQueryData = []struct { ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND has(attributes_string_key, 'user_name') group by ts order by ts", }, { - Name: "Test aggregate count on a with filter", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate count on a with filter", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, @@ -321,10 +287,11 @@ var testBuildLogsQueryData = []struct { ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND attributes_float64_value[indexOf(attributes_float64_key, 'bytes')] > 100.000000 AND has(attributes_string_key, 'user_name') group by ts order by ts", }, { - Name: "Test aggregate count distinct and order by value", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate count distinct and order by value", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "name", IsColumn: true}, @@ -336,10 +303,11 @@ var testBuildLogsQueryData = []struct { ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(name))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) group by ts order by value ASC,ts", }, { - Name: "Test aggregate count distinct on non selected field", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate count distinct on non selected field", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, @@ -350,10 +318,11 @@ var testBuildLogsQueryData = []struct { ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) group by ts order by ts", }, { - Name: "Test aggregate count distinct with filter and groupBy", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate count distinct with filter and groupBy", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "name", IsColumn: true}, @@ -365,7 +334,7 @@ var testBuildLogsQueryData = []struct { }, }, GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, - OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}, {ColumnName: "ts", Order: "ASC"}}, + OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}, {ColumnName: "ts", Order: "ASC", Key: "ts", IsColumn: true}}, }, TableName: "logs", ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," + @@ -378,10 +347,11 @@ var testBuildLogsQueryData = []struct { "order by method ASC,ts", }, { - Name: "Test aggregate count with multiple filter,groupBy and orderBy", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate count with multiple filter,groupBy and orderBy", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "name", IsColumn: true}, @@ -408,10 +378,11 @@ var testBuildLogsQueryData = []struct { "order by method ASC,x ASC,ts", }, { - Name: "Test aggregate avg", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate avg", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "bytes", DataType: v3.AttributeKeyDataTypeFloat64, Type: v3.AttributeKeyTypeTag}, @@ -422,7 +393,7 @@ var testBuildLogsQueryData = []struct { }, }, GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, - OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}, {ColumnName: "x", Order: "ASC"}}, + OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}, {ColumnName: "x", Order: "ASC", Key: "x", IsColumn: true}}, }, TableName: "logs", ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," + @@ -436,10 +407,11 @@ var testBuildLogsQueryData = []struct { "order by method ASC,ts", }, { - Name: "Test aggregate sum", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate sum", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true}, @@ -464,10 +436,11 @@ var testBuildLogsQueryData = []struct { "order by method ASC,ts", }, { - Name: "Test aggregate min", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate min", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true}, @@ -492,10 +465,11 @@ var testBuildLogsQueryData = []struct { "order by method ASC,ts", }, { - Name: "Test aggregate max", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate max", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true}, @@ -520,10 +494,11 @@ var testBuildLogsQueryData = []struct { "order by method ASC,ts", }, { - Name: "Test aggregate PXX", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate PXX", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true}, @@ -544,10 +519,11 @@ var testBuildLogsQueryData = []struct { "order by method ASC,ts", }, { - Name: "Test aggregate RateSum", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate RateSum", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true}, @@ -565,10 +541,11 @@ var testBuildLogsQueryData = []struct { "group by method,ts order by method ASC,ts", }, { - Name: "Test aggregate rate", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate rate", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "bytes", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeFloat64}, @@ -587,10 +564,11 @@ var testBuildLogsQueryData = []struct { "order by method ASC,ts", }, { - Name: "Test aggregate RateSum without materialized column", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate RateSum without materialized column", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "bytes", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeFloat64}, @@ -610,29 +588,29 @@ var testBuildLogsQueryData = []struct { "order by method ASC,ts", }, { - Name: "Test Noop", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test Noop", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ SelectColumns: []v3.AttributeKey{}, QueryName: "A", AggregateOperator: v3.AggregateOperatorNoOp, Expression: "A", Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}, - // GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, - // OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}}, }, ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," + "CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64," + "CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " + - "from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) ", + "from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) order by timestamp", }, { - Name: "Test aggregate with having clause", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate with having clause", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, @@ -650,10 +628,11 @@ var testBuildLogsQueryData = []struct { ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) group by ts having value > 10 order by ts", }, { - Name: "Test aggregate with having clause and filters", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test aggregate with having clause and filters", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, @@ -675,10 +654,11 @@ var testBuildLogsQueryData = []struct { ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' group by ts having value > 10 order by ts", }, { - Name: "Test top level key", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test top level key", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, @@ -700,10 +680,11 @@ var testBuildLogsQueryData = []struct { ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND body ILIKE '%test%' group by ts having value > 10 order by ts", }, { - Name: "Test attribute with same name as top level key", - Start: 1680066360726210000, - End: 1680066458000000000, - Step: 60, + Name: "Test attribute with same name as top level key", + PanelType: v3.PanelTypeGraph, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, BuilderQuery: &v3.BuilderQuery{ QueryName: "A", AggregateAttribute: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, @@ -729,7 +710,7 @@ var testBuildLogsQueryData = []struct { func TestBuildLogsQuery(t *testing.T) { for _, tt := range testBuildLogsQueryData { Convey("TestBuildLogsQuery", t, func() { - query, err := buildLogsQuery(tt.Start, tt.End, tt.Step, tt.BuilderQuery, map[string]v3.AttributeKey{}) + query, err := buildLogsQuery(tt.PanelType, tt.Start, tt.End, tt.Step, tt.BuilderQuery) So(err, ShouldBeNil) So(query, ShouldEqual, tt.ExpectedQuery) @@ -768,13 +749,15 @@ func TestGetZerosForEpochNano(t *testing.T) { } var testOrderBy = []struct { - Name string - Items []v3.OrderBy - Tags []string - Result string + Name string + PanelType v3.PanelType + Items []v3.OrderBy + Tags []string + Result []string }{ { - Name: "Test 1", + Name: "Test 1", + PanelType: v3.PanelTypeGraph, Items: []v3.OrderBy{ { ColumnName: "name", @@ -786,10 +769,11 @@ var testOrderBy = []struct { }, }, Tags: []string{"name"}, - Result: "name asc,value desc", + Result: []string{"name asc", "value desc"}, }, { - Name: "Test 2", + Name: "Test 2", + PanelType: v3.PanelTypeGraph, Items: []v3.OrderBy{ { ColumnName: "name", @@ -801,10 +785,11 @@ var testOrderBy = []struct { }, }, Tags: []string{"name", "bytes"}, - Result: "name asc,bytes asc", + Result: []string{"name asc", "bytes asc"}, }, { - Name: "Test 3", + Name: "Test 3", + PanelType: v3.PanelTypeList, Items: []v3.OrderBy{ { ColumnName: "name", @@ -820,18 +805,42 @@ var testOrderBy = []struct { }, }, Tags: []string{"name", "bytes"}, - Result: "name asc,bytes asc,value asc", + Result: []string{"name asc", "bytes asc", "value asc"}, + }, + { + Name: "Test 4", + PanelType: v3.PanelTypeList, + Items: []v3.OrderBy{ + { + ColumnName: "name", + Order: "asc", + }, + { + ColumnName: constants.SigNozOrderByValue, + Order: "asc", + }, + { + ColumnName: "bytes", + Order: "asc", + }, + { + ColumnName: "response_time", + Order: "desc", + Key: "response_time", + Type: v3.AttributeKeyTypeTag, + DataType: v3.AttributeKeyDataTypeString, + }, + }, + Tags: []string{"name", "bytes"}, + Result: []string{"name asc", "bytes asc", "value asc", "attributes_string_value[indexOf(attributes_string_key, 'response_time')] desc"}, }, } func TestOrderBy(t *testing.T) { for _, tt := range testOrderBy { Convey("testOrderBy", t, func() { - res := orderBy(tt.Items, tt.Tags) - So(res, ShouldEqual, tt.Result) - - // So(multiplier, ShouldEqual, tt.Multiplier) - // So(tt.Epoch*multiplier, ShouldEqual, tt.Result) + res := orderBy(tt.PanelType, tt.Items, tt.Tags) + So(res, ShouldResemble, tt.Result) }) } } diff --git a/pkg/query-service/app/metrics/v3/query_builder.go b/pkg/query-service/app/metrics/v3/query_builder.go index 82995d9285..dc5aadb618 100644 --- a/pkg/query-service/app/metrics/v3/query_builder.go +++ b/pkg/query-service/app/metrics/v3/query_builder.go @@ -3,8 +3,10 @@ package v3 import ( "fmt" "strings" + "time" "go.signoz.io/signoz/pkg/query-service/constants" + "go.signoz.io/signoz/pkg/query-service/model" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/utils" ) @@ -403,3 +405,12 @@ func PrepareMetricQuery(start, end int64, queryType v3.QueryType, panelType v3.P } return query, err } + +func BuildPromQuery(promQuery *v3.PromQuery, step, start, end int64) *model.QueryRangeParams { + return &model.QueryRangeParams{ + Query: promQuery.Query, + Start: time.UnixMilli(start), + End: time.UnixMilli(end), + Step: time.Duration(step * int64(time.Second)), + } +} diff --git a/pkg/query-service/app/querier/querier.go b/pkg/query-service/app/querier/querier.go new file mode 100644 index 0000000000..9603b00ecd --- /dev/null +++ b/pkg/query-service/app/querier/querier.go @@ -0,0 +1,426 @@ +package querier + +import ( + "context" + "encoding/json" + "fmt" + "math" + "sort" + "strings" + "time" + + logsV3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3" + metricsV3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3" + tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3" + + "go.signoz.io/signoz/pkg/query-service/cache" + "go.signoz.io/signoz/pkg/query-service/cache/status" + "go.signoz.io/signoz/pkg/query-service/interfaces" + "go.signoz.io/signoz/pkg/query-service/model" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" + "go.uber.org/zap" +) + +type missInterval struct { + start, end int64 // in milliseconds +} + +type querier struct { + cache cache.Cache + reader interfaces.Reader + keyGenerator cache.KeyGenerator + + fluxInterval time.Duration + + // used for testing + // TODO(srikanthccv): remove this once we have a proper mock + testingMode bool + queriesExecuted []string + returnedSeries []*v3.Series + returnedErr error +} + +type QuerierOptions struct { + Reader interfaces.Reader + Cache cache.Cache + KeyGenerator cache.KeyGenerator + FluxInterval time.Duration + + // used for testing + TestingMode bool + ReturnedSeries []*v3.Series + ReturnedErr error +} + +func NewQuerier(opts QuerierOptions) interfaces.Querier { + return &querier{ + cache: opts.Cache, + reader: opts.Reader, + keyGenerator: opts.KeyGenerator, + fluxInterval: opts.FluxInterval, + + testingMode: opts.TestingMode, + returnedSeries: opts.ReturnedSeries, + returnedErr: opts.ReturnedErr, + } +} + +func (q *querier) execClickHouseQuery(ctx context.Context, query string) ([]*v3.Series, error) { + q.queriesExecuted = append(q.queriesExecuted, query) + if q.testingMode && q.reader == nil { + return q.returnedSeries, q.returnedErr + } + return q.reader.GetTimeSeriesResultV3(ctx, query) +} + +func (q *querier) execPromQuery(ctx context.Context, params *model.QueryRangeParams) ([]*v3.Series, error) { + q.queriesExecuted = append(q.queriesExecuted, params.Query) + if q.testingMode && q.reader == nil { + return q.returnedSeries, q.returnedErr + } + promResult, _, err := q.reader.GetQueryRangeResult(ctx, params) + if err != nil { + return nil, err + } + matrix, promErr := promResult.Matrix() + if promErr != nil { + return nil, promErr + } + var seriesList []*v3.Series + for _, v := range matrix { + var s v3.Series + s.Labels = v.Metric.Copy().Map() + for idx := range v.Points { + p := v.Points[idx] + s.Points = append(s.Points, v3.Point{Timestamp: p.T, Value: p.V}) + } + seriesList = append(seriesList, &s) + } + return seriesList, nil +} + +// findMissingTimeRanges finds the missing time ranges in the seriesList +// and returns a list of miss structs, It takes the fluxInterval into +// account to find the missing time ranges. +// +// The [End - fluxInterval, End] is always added to the list of misses, because +// the data might still be in flux and not yet available in the database. +func findMissingTimeRanges(start, end int64, seriesList []*v3.Series, fluxInterval time.Duration) (misses []missInterval) { + var cachedStart, cachedEnd int64 + for idx := range seriesList { + series := seriesList[idx] + for pointIdx := range series.Points { + point := series.Points[pointIdx] + if cachedStart == 0 || point.Timestamp < cachedStart { + cachedStart = point.Timestamp + } + if cachedEnd == 0 || point.Timestamp > cachedEnd { + cachedEnd = point.Timestamp + } + } + } + + // Exclude the flux interval from the cached end time + cachedEnd = int64( + math.Min( + float64(cachedEnd), + float64(time.Now().UnixMilli()-fluxInterval.Milliseconds()), + ), + ) + + // There are five cases to consider + // 1. Cached time range is a subset of the requested time range + // 2. Cached time range is a superset of the requested time range + // 3. Cached time range is a left overlap of the requested time range + // 4. Cached time range is a right overlap of the requested time range + // 5. Cached time range is a disjoint of the requested time range + if cachedStart >= start && cachedEnd <= end { + // Case 1: Cached time range is a subset of the requested time range + // Add misses for the left and right sides of the cached time range + misses = append(misses, missInterval{start: start, end: cachedStart - 1}) + misses = append(misses, missInterval{start: cachedEnd + 1, end: end}) + } else if cachedStart <= start && cachedEnd >= end { + // Case 2: Cached time range is a superset of the requested time range + // No misses + } else if cachedStart <= start && cachedEnd >= start { + // Case 3: Cached time range is a left overlap of the requested time range + // Add a miss for the left side of the cached time range + misses = append(misses, missInterval{start: cachedEnd + 1, end: end}) + } else if cachedStart <= end && cachedEnd >= end { + // Case 4: Cached time range is a right overlap of the requested time range + // Add a miss for the right side of the cached time range + misses = append(misses, missInterval{start: start, end: cachedStart - 1}) + } else { + // Case 5: Cached time range is a disjoint of the requested time range + // Add a miss for the entire requested time range + misses = append(misses, missInterval{start: start, end: end}) + } + + // remove the struts with start > end + var validMisses []missInterval + for idx := range misses { + miss := misses[idx] + if miss.start <= miss.end { + validMisses = append(validMisses, miss) + } + } + return validMisses +} + +// findMissingTimeRanges finds the missing time ranges in the cached data +// and returns them as a list of misses +func (q *querier) findMissingTimeRanges(start, end, step int64, cachedData []byte) (misses []missInterval) { + var cachedSeriesList []*v3.Series + if err := json.Unmarshal(cachedData, &cachedSeriesList); err != nil { + // In case of error, we return the entire range as a miss + return []missInterval{{start: start, end: end}} + } + return findMissingTimeRanges(start, end, cachedSeriesList, q.fluxInterval) +} + +func labelsToString(labels map[string]string) string { + type label struct { + Key string + Value string + } + var labelsList []label + for k, v := range labels { + labelsList = append(labelsList, label{Key: k, Value: v}) + } + sort.Slice(labelsList, func(i, j int) bool { + return labelsList[i].Key < labelsList[j].Key + }) + labelKVs := make([]string, len(labelsList)) + for idx := range labelsList { + labelKVs[idx] = labelsList[idx].Key + "=" + labelsList[idx].Value + } + return fmt.Sprintf("{%s}", strings.Join(labelKVs, ",")) +} + +func mergeSerieses(cachedSeries, missedSeries []*v3.Series) []*v3.Series { + // Merge the missed series with the cached series by timestamp + mergedSeries := make([]*v3.Series, 0) + seriesesByLabels := make(map[string]*v3.Series) + for idx := range cachedSeries { + series := cachedSeries[idx] + seriesesByLabels[labelsToString(series.Labels)] = series + } + + for idx := range missedSeries { + series := missedSeries[idx] + if _, ok := seriesesByLabels[labelsToString(series.Labels)]; !ok { + seriesesByLabels[labelsToString(series.Labels)] = series + continue + } + seriesesByLabels[labelsToString(series.Labels)].Points = append(seriesesByLabels[labelsToString(series.Labels)].Points, series.Points...) + } + // Sort the points in each series by timestamp + for idx := range seriesesByLabels { + series := seriesesByLabels[idx] + series.SortPoints() + mergedSeries = append(mergedSeries, series) + } + return mergedSeries +} + +func (q *querier) runBuilderQueries(ctx context.Context, params *v3.QueryRangeParamsV3, keys map[string]v3.AttributeKey) ([]*v3.Series, error, map[string]string) { + + cacheKeys := q.keyGenerator.GenerateKeys(params) + + seriesList := make([]*v3.Series, 0) + errQueriesByName := make(map[string]string) + var err error + + for queryName, builderQuery := range params.CompositeQuery.BuilderQueries { + + // TODO: add support for logs and traces + if builderQuery.DataSource == v3.DataSourceLogs { + query, err := logsV3.PrepareLogsQuery(params.Start, params.End, params.CompositeQuery.QueryType, params.CompositeQuery.PanelType, builderQuery) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + series, err := q.execClickHouseQuery(ctx, query) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + seriesList = append(seriesList, series...) + continue + } + + if builderQuery.DataSource == v3.DataSourceTraces { + query, err := tracesV3.PrepareTracesQuery(params.Start, params.End, params.CompositeQuery.QueryType, params.CompositeQuery.PanelType, builderQuery, keys) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + + series, err := q.execClickHouseQuery(ctx, query) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + seriesList = append(seriesList, series...) + continue + } + + cacheKey := cacheKeys[queryName] + var cachedData []byte + if !params.NoCache { + var retrieveStatus status.RetrieveStatus + cachedData, retrieveStatus, err = q.cache.Retrieve(cacheKey, true) + zap.L().Debug("cache retrieve status", zap.String("status", retrieveStatus.String())) + if err != nil { + return nil, err, nil + } + } + misses := q.findMissingTimeRanges(params.Start, params.End, params.Step, cachedData) + missedSeries := make([]*v3.Series, 0) + cachedSeries := make([]*v3.Series, 0) + for _, miss := range misses { + query, err := metricsV3.PrepareMetricQuery( + miss.start, + miss.end, + params.CompositeQuery.QueryType, + params.CompositeQuery.PanelType, + builderQuery, + ) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + series, err := q.execClickHouseQuery(ctx, query) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + missedSeries = append(missedSeries, series...) + } + if err := json.Unmarshal(cachedData, &cachedSeries); err != nil && cachedData != nil { + errQueriesByName[queryName] = err.Error() + continue + } + mergedSeries := mergeSerieses(cachedSeries, missedSeries) + + seriesList = append(seriesList, mergedSeries...) + // Cache the seriesList for future queries + if len(missedSeries) > 0 { + mergedSeriesData, err := json.Marshal(mergedSeries) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + err = q.cache.Store(cacheKey, mergedSeriesData, time.Hour) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + } + } + if len(errQueriesByName) > 0 { + err = fmt.Errorf("error in builder queries") + } + return seriesList, err, errQueriesByName +} + +func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParamsV3) ([]*v3.Series, error, map[string]string) { + seriesList := make([]*v3.Series, 0) + errQueriesByName := make(map[string]string) + var err error + for queryName, promQuery := range params.CompositeQuery.PromQueries { + cacheKey := q.keyGenerator.GenerateKeys(params)[queryName] + var cachedData []byte + var retrieveStatus status.RetrieveStatus + if !params.NoCache { + cachedData, retrieveStatus, err = q.cache.Retrieve(cacheKey, true) + zap.L().Debug("cache retrieve status", zap.String("status", retrieveStatus.String())) + } + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + misses := q.findMissingTimeRanges(params.Start, params.End, params.Step, cachedData) + missedSeries := make([]*v3.Series, 0) + cachedSeries := make([]*v3.Series, 0) + for _, miss := range misses { + query := metricsV3.BuildPromQuery( + promQuery, + params.Step, + miss.start, + miss.end, + ) + series, err := q.execPromQuery(ctx, query) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + missedSeries = append(missedSeries, series...) + } + if err := json.Unmarshal(cachedData, &cachedSeries); err != nil && cachedData != nil { + errQueriesByName[queryName] = err.Error() + continue + } + mergedSeries := mergeSerieses(cachedSeries, missedSeries) + + seriesList = append(seriesList, mergedSeries...) + // Cache the seriesList for future queries + if len(missedSeries) > 0 { + mergedSeriesData, err := json.Marshal(mergedSeries) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + err = q.cache.Store(cacheKey, mergedSeriesData, time.Hour) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + } + } + if len(errQueriesByName) > 0 { + err = fmt.Errorf("error in prom queries") + } + return seriesList, err, errQueriesByName +} + +func (q *querier) runClickHouseQueries(ctx context.Context, params *v3.QueryRangeParamsV3) ([]*v3.Series, error, map[string]string) { + seriesList := make([]*v3.Series, 0) + errQueriesByName := make(map[string]string) + var err error + for queryName, clickHouseQuery := range params.CompositeQuery.ClickHouseQueries { + series, err := q.execClickHouseQuery(ctx, clickHouseQuery.Query) + if err != nil { + errQueriesByName[queryName] = err.Error() + continue + } + seriesList = append(seriesList, series...) + } + if len(errQueriesByName) > 0 { + err = fmt.Errorf("error in clickhouse queries") + } + return seriesList, err, errQueriesByName +} + +func (q *querier) QueryRange(ctx context.Context, params *v3.QueryRangeParamsV3, keys map[string]v3.AttributeKey) ([]*v3.Series, error, map[string]string) { + var seriesList []*v3.Series + var err error + var errQueriesByName map[string]string + if params.CompositeQuery != nil { + switch params.CompositeQuery.QueryType { + case v3.QueryTypeBuilder: + seriesList, err, errQueriesByName = q.runBuilderQueries(ctx, params, keys) + case v3.QueryTypePromQL: + seriesList, err, errQueriesByName = q.runPromQueries(ctx, params) + case v3.QueryTypeClickHouseSQL: + seriesList, err, errQueriesByName = q.runClickHouseQueries(ctx, params) + default: + err = fmt.Errorf("invalid query type") + } + } + return seriesList, err, errQueriesByName +} + +func (q *querier) QueriesExecuted() []string { + return q.queriesExecuted +} diff --git a/pkg/query-service/app/querier/querier_test.go b/pkg/query-service/app/querier/querier_test.go new file mode 100644 index 0000000000..51293ad493 --- /dev/null +++ b/pkg/query-service/app/querier/querier_test.go @@ -0,0 +1,507 @@ +package querier + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "go.signoz.io/signoz/pkg/query-service/app/queryBuilder" + "go.signoz.io/signoz/pkg/query-service/cache/inmemory" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" +) + +func TestFindMissingTimeRangesZeroFreshNess(t *testing.T) { + // There are five scenarios: + // 1. Cached time range is a subset of the requested time range + // 2. Cached time range is a superset of the requested time range + // 3. Cached time range is a left overlap of the requested time range + // 4. Cached time range is a right overlap of the requested time range + // 5. Cached time range is a disjoint of the requested time range + testCases := []struct { + name string + requestedStart int64 // in milliseconds + requestedEnd int64 // in milliseconds + cachedSeries []*v3.Series + expectedMiss []missInterval + }{ + { + name: "cached time range is a subset of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722 + 60*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 120*60*1000, + Value: 1, + }, + }, + }, + }, + expectedMiss: []missInterval{ + { + start: 1675115596722, + end: 1675115596722 + 60*60*1000 - 1, + }, + { + start: 1675115596722 + 120*60*1000 + 1, + end: 1675115596722 + 180*60*1000, + }, + }, + }, + { + name: "cached time range is a superset of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722, + Value: 1, + }, + { + Timestamp: 1675115596722 + 60*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 120*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 180*60*1000, + Value: 1, + }, + }, + }, + }, + expectedMiss: []missInterval{}, + }, + { + name: "cached time range is a left overlap of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722, + Value: 1, + }, + { + Timestamp: 1675115596722 + 60*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 120*60*1000, + Value: 1, + }, + }, + }, + }, + expectedMiss: []missInterval{ + { + start: 1675115596722 + 120*60*1000 + 1, + end: 1675115596722 + 180*60*1000, + }, + }, + }, + { + name: "cached time range is a right overlap of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722 + 60*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 120*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 180*60*1000, + Value: 1, + }, + }, + }, + }, + expectedMiss: []missInterval{ + { + start: 1675115596722, + end: 1675115596722 + 60*60*1000 - 1, + }, + }, + }, + { + name: "cached time range is a disjoint of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722 + 240*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 300*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 360*60*1000, + Value: 1, + }, + }, + }, + }, + expectedMiss: []missInterval{ + { + start: 1675115596722, + end: 1675115596722 + 180*60*1000, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + misses := findMissingTimeRanges(tc.requestedStart, tc.requestedEnd, tc.cachedSeries, 0*time.Minute) + if len(misses) != len(tc.expectedMiss) { + t.Errorf("expected %d misses, got %d", len(tc.expectedMiss), len(misses)) + } + for i, miss := range misses { + if miss.start != tc.expectedMiss[i].start { + t.Errorf("expected start %d, got %d", tc.expectedMiss[i].start, miss.start) + } + if miss.end != tc.expectedMiss[i].end { + t.Errorf("expected end %d, got %d", tc.expectedMiss[i].end, miss.end) + } + } + }) + } +} + +func TestFindMissingTimeRangesWithFluxInterval(t *testing.T) { + + testCases := []struct { + name string + requestedStart int64 + requestedEnd int64 + cachedSeries []*v3.Series + fluxInterval time.Duration + expectedMiss []missInterval + }{ + { + name: "cached time range is a subset of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722 + 60*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 120*60*1000, + Value: 1, + }, + }, + }, + }, + fluxInterval: 5 * time.Minute, + expectedMiss: []missInterval{ + { + start: 1675115596722, + end: 1675115596722 + 60*60*1000 - 1, + }, + { + start: 1675115596722 + 120*60*1000 + 1, + end: 1675115596722 + 180*60*1000, + }, + }, + }, + { + name: "cached time range is a superset of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722, + Value: 1, + }, + { + Timestamp: 1675115596722 + 60*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 120*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 180*60*1000, + Value: 1, + }, + }, + }, + }, + fluxInterval: 5 * time.Minute, + expectedMiss: []missInterval{}, + }, + { + name: "cache time range is a left overlap of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722, + Value: 1, + }, + { + Timestamp: 1675115596722 + 60*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 120*60*1000, + Value: 1, + }, + }, + }, + }, + fluxInterval: 5 * time.Minute, + expectedMiss: []missInterval{ + { + start: 1675115596722 + 120*60*1000 + 1, + end: 1675115596722 + 180*60*1000, + }, + }, + }, + { + name: "cache time range is a right overlap of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722 + 60*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 120*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 180*60*1000, + Value: 1, + }, + }, + }, + }, + fluxInterval: 5 * time.Minute, + expectedMiss: []missInterval{ + { + start: 1675115596722, + end: 1675115596722 + 60*60*1000 - 1, + }, + }, + }, + { + name: "cache time range is a disjoint of the requested time range", + requestedStart: 1675115596722, + requestedEnd: 1675115596722 + 180*60*1000, + cachedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + { + Timestamp: 1675115596722 + 240*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 300*60*1000, + Value: 1, + }, + { + Timestamp: 1675115596722 + 360*60*1000, + Value: 1, + }, + }, + }, + }, + fluxInterval: 5 * time.Minute, + expectedMiss: []missInterval{ + { + start: 1675115596722, + end: 1675115596722 + 180*60*1000, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + misses := findMissingTimeRanges(tc.requestedStart, tc.requestedEnd, tc.cachedSeries, tc.fluxInterval) + if len(misses) != len(tc.expectedMiss) { + t.Errorf("expected %d misses, got %d", len(tc.expectedMiss), len(misses)) + } + for i, miss := range misses { + if miss.start != tc.expectedMiss[i].start { + t.Errorf("expected start %d, got %d", tc.expectedMiss[i].start, miss.start) + } + if miss.end != tc.expectedMiss[i].end { + t.Errorf("expected end %d, got %d", tc.expectedMiss[i].end, miss.end) + } + } + }) + } +} + +func TestQueryRange(t *testing.T) { + params := []*v3.QueryRangeParamsV3{ + { + Start: 1675115596722, + End: 1675115596722 + 120*60*1000, + Step: 5 * time.Minute.Microseconds(), + CompositeQuery: &v3.CompositeQuery{ + QueryType: v3.QueryTypeBuilder, + BuilderQueries: map[string]*v3.BuilderQuery{ + "A": { + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "http_server_requests_seconds_count", Type: v3.AttributeKeyTypeUnspecified, DataType: "float64", IsColumn: true}, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{Key: "method", IsColumn: false}, + Operator: "=", + Value: "GET", + }, + }, + }, + GroupBy: []v3.AttributeKey{ + {Key: "service_name", IsColumn: false}, + {Key: "method", IsColumn: false}, + }, + AggregateOperator: v3.AggregateOperatorSumRate, + Expression: "A", + }, + }, + }, + }, + { + Start: 1675115596722 + 60*60*1000, + End: 1675115596722 + 180*60*1000, + Step: 5 * time.Minute.Microseconds(), + CompositeQuery: &v3.CompositeQuery{ + QueryType: v3.QueryTypeBuilder, + BuilderQueries: map[string]*v3.BuilderQuery{ + "A": { + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "http_server_requests_seconds_count", Type: v3.AttributeKeyTypeUnspecified, DataType: "float64", IsColumn: true}, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{Key: "method", IsColumn: false}, + Operator: "=", + Value: "GET", + }, + }, + }, + GroupBy: []v3.AttributeKey{ + {Key: "service_name", IsColumn: false}, + {Key: "method", IsColumn: false}, + }, + AggregateOperator: v3.AggregateOperatorSumRate, + Expression: "A", + }, + }, + }, + }, + } + cache := inmemory.New(&inmemory.Options{TTL: 5 * time.Minute, CleanupInterval: 10 * time.Minute}) + opts := QuerierOptions{ + Cache: cache, + Reader: nil, + FluxInterval: 5 * time.Minute, + KeyGenerator: queryBuilder.NewKeyGenerator(), + + TestingMode: true, + ReturnedSeries: []*v3.Series{ + { + Labels: map[string]string{ + "method": "GET", + "service_name": "test", + "__name__": "http_server_requests_seconds_count", + }, + Points: []v3.Point{ + {Timestamp: 1675115596722, Value: 1}, + {Timestamp: 1675115596722 + 60*60*1000, Value: 2}, + {Timestamp: 1675115596722 + 120*60*1000, Value: 3}, + }, + }, + }, + } + q := NewQuerier(opts) + expectedTimeRangeInQueryString := []string{ + fmt.Sprintf("timestamp_ms >= %d AND timestamp_ms <= %d", 1675115596722, 1675115596722+120*60*1000), + fmt.Sprintf("timestamp_ms >= %d AND timestamp_ms <= %d", 1675115596722+120*60*1000+1, 1675115596722+180*60*1000), + } + + for i, param := range params { + _, err, errByName := q.QueryRange(context.Background(), param, nil) + if err != nil { + t.Errorf("expected no error, got %s", err) + } + if len(errByName) > 0 { + t.Errorf("expected no error, got %v", errByName) + } + + if !strings.Contains(q.QueriesExecuted()[i], expectedTimeRangeInQueryString[i]) { + t.Errorf("expected query to contain %s, got %s", expectedTimeRangeInQueryString[i], q.QueriesExecuted()[i]) + } + } +} diff --git a/pkg/query-service/app/queryBuilder/query_builder.go b/pkg/query-service/app/queryBuilder/query_builder.go index 173a139aa9..038b06f312 100644 --- a/pkg/query-service/app/queryBuilder/query_builder.go +++ b/pkg/query-service/app/queryBuilder/query_builder.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/SigNoz/govaluate" + "go.signoz.io/signoz/pkg/query-service/cache" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.uber.org/zap" ) @@ -36,7 +37,7 @@ var SupportedFunctions = []string{ var EvalFuncs = map[string]govaluate.ExpressionFunction{} type prepareTracesQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery, keys map[string]v3.AttributeKey) (string, error) -type prepareLogsQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery, fields map[string]v3.AttributeKey) (string, error) +type prepareLogsQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error) type prepareMetricQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error) type QueryBuilder struct { @@ -140,8 +141,8 @@ func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3, args ...in switch query.DataSource { case v3.DataSourceTraces: keys := map[string]v3.AttributeKey{} - if len(args) == 2 { - keys = args[1].(map[string]v3.AttributeKey) + if len(args) > 0 { + keys = args[0].(map[string]v3.AttributeKey) } queryString, err := qb.options.BuildTraceQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, keys) if err != nil { @@ -149,11 +150,7 @@ func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3, args ...in } queries[queryName] = queryString case v3.DataSourceLogs: - fields := map[string]v3.AttributeKey{} - if len(args) == 1 { - fields = args[0].(map[string]v3.AttributeKey) - } - queryString, err := qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, fields) + queryString, err := qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query) if err != nil { return nil, err } @@ -192,3 +189,82 @@ func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3, args ...in } return queries, nil } + +// cacheKeyGenerator implements the cache.KeyGenerator interface +type cacheKeyGenerator struct { +} + +func expressionToKey(expression *govaluate.EvaluableExpression, keys map[string]string) string { + + var modified []govaluate.ExpressionToken + tokens := expression.Tokens() + for idx := range tokens { + token := tokens[idx] + if token.Kind == govaluate.VARIABLE { + token.Value = keys[fmt.Sprintf("%s", token.Value)] + token.Meta = keys[fmt.Sprintf("%s", token.Meta)] + } + modified = append(modified, token) + } + // err should be nil here since the expression is already validated + formula, _ := govaluate.NewEvaluableExpressionFromTokens(modified) + return formula.ExpressionString() +} + +func (c *cacheKeyGenerator) GenerateKeys(params *v3.QueryRangeParamsV3) map[string]string { + keys := make(map[string]string) + + // Build keys for each builder query + for queryName, query := range params.CompositeQuery.BuilderQueries { + if query.Expression == queryName { + var parts []string + + // We need to build uniqe cache query for BuilderQuery + + parts = append(parts, fmt.Sprintf("source=%s", query.DataSource)) + parts = append(parts, fmt.Sprintf("step=%d", query.StepInterval)) + parts = append(parts, fmt.Sprintf("aggregate=%s", query.AggregateOperator)) + + if query.AggregateAttribute.Key != "" { + parts = append(parts, fmt.Sprintf("aggregateAttribute=%s", query.AggregateAttribute.CacheKey())) + } + + if query.Filters != nil && len(query.Filters.Items) > 0 { + for idx, filter := range query.Filters.Items { + parts = append(parts, fmt.Sprintf("filter-%d=%s", idx, filter.CacheKey())) + } + } + + if len(query.GroupBy) > 0 { + for idx, groupBy := range query.GroupBy { + parts = append(parts, fmt.Sprintf("groupBy-%d=%s", idx, groupBy.CacheKey())) + } + } + + if len(query.Having) > 0 { + for idx, having := range query.Having { + parts = append(parts, fmt.Sprintf("having-%d=%s", idx, having.CacheKey())) + } + } + + key := strings.Join(parts, "&") + keys[queryName] = key + } + } + + // Build keys for each expression + for _, query := range params.CompositeQuery.BuilderQueries { + if query.Expression != query.QueryName { + expression, _ := govaluate.NewEvaluableExpressionWithFunctions(query.Expression, EvalFuncs) + + expressionCacheKey := expressionToKey(expression, keys) + keys[query.QueryName] = expressionCacheKey + } + } + + return keys +} + +func NewKeyGenerator() cache.KeyGenerator { + return &cacheKeyGenerator{} +} diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 78101ea406..10a172e000 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -220,7 +220,7 @@ func loggingMiddleware(next http.Handler) http.Handler { path, _ := route.GetPathTemplate() startTime := time.Now() next.ServeHTTP(w, r) - zap.S().Info(path, "\ttimeTaken: ", time.Now().Sub(startTime)) + zap.S().Info(path+"\ttimeTaken:"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path)) }) } @@ -232,7 +232,7 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler { path, _ := route.GetPathTemplate() startTime := time.Now() next.ServeHTTP(w, r) - zap.S().Info(path, "\tprivatePort: true", "\ttimeTaken: ", time.Now().Sub(startTime)) + zap.S().Info(path+"\tprivatePort: true \ttimeTaken"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path), zap.Bool("tprivatePort", true)) }) } diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index ee7f858dbc..d86d6df205 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -46,6 +46,9 @@ func GetAlertManagerApiPrefix() string { // Alert manager channel subpath var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/routes") +var OTLPTarget = GetOrDefaultEnv("OTLP_TARGET", "") +var LogExportBatchSize = GetOrDefaultEnv("LOG_EXPORT_BATCH_SIZE", "1000") + var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db") var DurationSortFeature = GetOrDefaultEnv("DURATION_SORT_FEATURE", "true") @@ -291,3 +294,5 @@ var StaticFieldsLogsV3 = map[string]v3.AttributeKey{ } const SigNozOrderByValue = "#SIGNOZ_VALUE" + +const TIMESTAMP = "timestamp" diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index cdc1f3387d..c4d9bbdbb6 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -36,9 +36,9 @@ type Reader interface { GetDisks(ctx context.Context) (*[]model.DiskItem, *model.ApiError) GetSpanFilters(ctx context.Context, query *model.SpanFilterParams) (*model.SpanFiltersResponse, *model.ApiError) GetTraceAggregateAttributes(ctx context.Context, req *v3.AggregateAttributeRequest) (*v3.AggregateAttributeResponse, error) - GetTraceAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) + GetTraceAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) GetTraceAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) - GetSpanAttributeKeys(ctx context.Context) (map[string]v3.AttributeKey, error) + GetSpanAttributeKeys(ctx context.Context) (map[string]v3.AttributeKey, error) GetTagFilters(ctx context.Context, query *model.TagFilterParams) (*model.TagFilters, *model.ApiError) GetTagValues(ctx context.Context, query *model.TagFilterParams) (*model.TagValues, *model.ApiError) GetFilteredSpans(ctx context.Context, query *model.GetFilteredSpansParams) (*model.GetFilterSpansResponse, *model.ApiError) @@ -94,3 +94,10 @@ type Reader interface { QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error) CheckClickHouse(ctx context.Context) error } + +type Querier interface { + QueryRange(context.Context, *v3.QueryRangeParamsV3, map[string]v3.AttributeKey) ([]*v3.Series, error, map[string]string) + + // test helpers + QueriesExecuted() []string +} diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index e323f7e47c..7d3028b3d8 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -3,6 +3,7 @@ package v3 import ( "encoding/json" "fmt" + "sort" "strconv" "time" @@ -283,6 +284,10 @@ type AttributeKey struct { IsColumn bool `json:"isColumn"` } +func (a AttributeKey) CacheKey() string { + return fmt.Sprintf("%s-%s-%s-%t", a.Key, a.DataType, a.Type, a.IsColumn) +} + func (a AttributeKey) Validate() error { switch a.DataType { case AttributeKeyDataTypeBool, AttributeKeyDataTypeInt64, AttributeKeyDataTypeFloat64, AttributeKeyDataTypeString, AttributeKeyDataTypeUnspecified: @@ -319,6 +324,7 @@ type QueryRangeParamsV3 struct { Step int64 `json:"step"` CompositeQuery *CompositeQuery `json:"compositeQuery"` Variables map[string]interface{} `json:"variables,omitempty"` + NoCache bool `json:"noCache"` } type PromQuery struct { @@ -534,9 +540,17 @@ type FilterItem struct { Operator FilterOperator `json:"op"` } +func (f *FilterItem) CacheKey() string { + return fmt.Sprintf("key:%s,op:%s,value:%v", f.Key.CacheKey(), f.Operator, f.Value) +} + type OrderBy struct { - ColumnName string `json:"columnName"` - Order string `json:"order"` + ColumnName string `json:"columnName"` + Order string `json:"order"` + Key string `json:"-"` + DataType AttributeKeyDataType `json:"-"` + Type AttributeKeyType `json:"-"` + IsColumn bool `json:"-"` } type Having struct { @@ -545,6 +559,10 @@ type Having struct { Value interface{} `json:"value"` } +func (h *Having) CacheKey() string { + return fmt.Sprintf("column:%s,op:%s,value:%v", h.ColumnName, h.Operator, h.Value) +} + type QueryRangeResponse struct { ResultType string `json:"resultType"` Result []*Result `json:"result"` @@ -561,6 +579,12 @@ type Series struct { Points []Point `json:"values"` } +func (s *Series) SortPoints() { + sort.Slice(s.Points, func(i, j int) bool { + return s.Points[i].Timestamp < s.Points[j].Timestamp + }) +} + type Row struct { Timestamp time.Time `json:"timestamp"` Data map[string]interface{} `json:"data"` @@ -577,6 +601,21 @@ func (p *Point) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]interface{}{"timestamp": p.Timestamp, "value": v}) } +// UnmarshalJSON implements json.Unmarshaler. +func (p *Point) UnmarshalJSON(data []byte) error { + var v struct { + Timestamp int64 `json:"timestamp"` + Value string `json:"value"` + } + if err := json.Unmarshal(data, &v); err != nil { + return err + } + p.Timestamp = v.Timestamp + var err error + p.Value, err = strconv.ParseFloat(v.Value, 64) + return err +} + // ExploreQuery is a query for the explore page // It is a composite query with a source page name // The source page name is used to identify the page that initiated the query diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index fcb9c53175..057fa66ba2 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -169,7 +169,7 @@ services: <<: *clickhouse-depends otel-collector: - image: signoz/signoz-otel-collector:0.76.1 + image: signoz/signoz-otel-collector:0.79.1 command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] user: root # required for reading docker container logs volumes: @@ -195,7 +195,7 @@ services: <<: *clickhouse-depends otel-collector-metrics: - image: signoz/signoz-otel-collector:0.76.1 + image: signoz/signoz-otel-collector:0.79.1 command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml