diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f5f5e5610a..1d8d4e7b70 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@v3 + uses: actions/checkout@v4 - 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@v3 + uses: actions/checkout@v4 - name: Create .env file run: | echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > frontend/.env @@ -54,12 +54,12 @@ jobs: build-query-service: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Setup golang uses: actions/setup-go@v4 with: go-version: "1.21" - - name: Checkout code - uses: actions/checkout@v3 - name: Run tests shell: bash run: | @@ -72,12 +72,12 @@ jobs: build-ee-query-service: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Setup golang uses: actions/setup-go@v4 with: go-version: "1.21" - - name: Checkout code - 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 14a0c127aa..be02f3bb82 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index b624a90b9f..3a38338cf0 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -7,7 +7,7 @@ jobs: lint-commits: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v5 diff --git a/.github/workflows/create-issue-on-pr-merge.yml b/.github/workflows/create-issue-on-pr-merge.yml index 2b0c849ffa..2a79618d12 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@v3 + uses: actions/checkout@v4 with: repository: signoz/gh-bot - name: Use Node v16 - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16 - name: Setup Cache & Install Dependencies diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 053a8733dc..be454590f3 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 'Dependency Review' with: fail-on-severity: high diff --git a/.github/workflows/e2e-k3s.yaml b/.github/workflows/e2e-k3s.yaml index 71061bfc73..770a2f4df3 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@v3 + uses: actions/checkout@v4 - name: Build query-service image env: diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index d6c05dfd6f..9ad3ef4313 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@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: "16.x" - name: Install dependencies diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 98ee1e0fc4..f8eb005883 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -14,7 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + - name: Setup golang + uses: actions/setup-go@v4 + with: + go-version: "1.21" - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx @@ -42,6 +46,11 @@ jobs: else echo "DOCKER_TAG=${{ steps.branch-name.outputs.current_branch }}-oss" >> $GITHUB_ENV fi + - name: Install cross-compilation tools + run: | + set -ex + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools - name: Build and push docker image run: make build-push-query-service @@ -49,7 +58,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + - name: Setup golang + uses: actions/setup-go@v4 + with: + go-version: "1.21" - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx @@ -77,6 +90,11 @@ jobs: else echo "DOCKER_TAG=${{ steps.branch-name.outputs.current_branch }}" >> $GITHUB_ENV fi + - name: Install cross-compilation tools + run: | + set -ex + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools - name: Build and push docker image run: make build-push-ee-query-service @@ -84,7 +102,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install dependencies working-directory: frontend run: yarn install @@ -128,7 +146,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create .env file run: | echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > frontend/.env diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 742768525f..8c62c12d1b 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Sonar analysis diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml index 6de51f4733..21ea7a3c75 100644 --- a/.github/workflows/staging-deployment.yaml +++ b/.github/workflows/staging-deployment.yaml @@ -26,6 +26,7 @@ jobs: echo "GITHUB_SHA: ${GITHUB_SHA}" export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it export OTELCOL_TAG="main" + export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work docker system prune --force docker pull signoz/signoz-otel-collector:main cd ~/signoz diff --git a/.github/workflows/testing-deployment.yaml b/.github/workflows/testing-deployment.yaml index d65a4e8bbc..799222ee3e 100644 --- a/.github/workflows/testing-deployment.yaml +++ b/.github/workflows/testing-deployment.yaml @@ -26,6 +26,7 @@ jobs: echo "GITHUB_SHA: ${GITHUB_SHA}" export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it export DEV_BUILD="1" + export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work docker system prune --force cd ~/signoz git status diff --git a/Makefile b/Makefile index 7d976341c1..5213c4597a 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ BUILD_HASH ?= $(shell git rev-parse --short HEAD) BUILD_TIME ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") BUILD_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) DEV_LICENSE_SIGNOZ_IO ?= https://staging-license.signoz.io/api/v1 +DEV_BUILD ?= "" # set to any non-empty value to enable dev build # Internal variables or constants. FRONTEND_DIRECTORY ?= frontend @@ -15,15 +16,15 @@ QUERY_SERVICE_DIRECTORY ?= pkg/query-service EE_QUERY_SERVICE_DIRECTORY ?= ee/query-service STANDALONE_DIRECTORY ?= deploy/docker/clickhouse-setup SWARM_DIRECTORY ?= deploy/docker-swarm/clickhouse-setup -LOCAL_GOOS ?= $(shell go env GOOS) -LOCAL_GOARCH ?= $(shell go env GOARCH) + +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) +GOPATH ?= $(shell go env GOPATH) REPONAME ?= signoz DOCKER_TAG ?= $(subst v,,$(BUILD_VERSION)) - FRONTEND_DOCKER_IMAGE ?= frontend QUERY_SERVICE_DOCKER_IMAGE ?= query-service -DEV_BUILD ?= "" # Build-time Go variables PACKAGE?=go.signoz.io/signoz @@ -37,10 +38,22 @@ LD_FLAGS=-X ${buildHash}=${BUILD_HASH} -X ${buildTime}=${BUILD_TIME} -X ${buildV DEV_LD_FLAGS=-X ${licenseSignozIo}=${DEV_LICENSE_SIGNOZ_IO} all: build-push-frontend build-push-query-service + +# Steps to build static files of frontend +build-frontend-static: + @echo "------------------" + @echo "--> Building frontend static files" + @echo "------------------" + @cd $(FRONTEND_DIRECTORY) && \ + rm -rf build && \ + CI=1 yarn install && \ + yarn build && \ + ls -l build + # Steps to build and push docker image of frontend .PHONY: build-frontend-amd64 build-push-frontend # Step to build docker image of frontend in amd64 (used in build pipeline) -build-frontend-amd64: +build-frontend-amd64: build-frontend-static @echo "------------------" @echo "--> Building frontend docker image for amd64" @echo "------------------" @@ -49,7 +62,7 @@ build-frontend-amd64: --build-arg TARGETPLATFORM="linux/amd64" . # Step to build and push docker image of frontend(used in push pipeline) -build-push-frontend: +build-push-frontend: build-frontend-static @echo "------------------" @echo "--> Building and pushing frontend docker image" @echo "------------------" @@ -57,24 +70,52 @@ build-push-frontend: docker buildx build --file Dockerfile --progress plain --push --platform linux/arm64,linux/amd64 \ --tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) . +# Steps to build static binary of query service +.PHONY: build-query-service-static +build-query-service-static: + @echo "------------------" + @echo "--> Building query-service static binary" + @echo "------------------" + @if [ $(DEV_BUILD) != "" ]; then \ + cd $(QUERY_SERVICE_DIRECTORY) && \ + CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \ + -ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \ + else \ + cd $(QUERY_SERVICE_DIRECTORY) && \ + CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \ + -ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS}"; \ + fi + +.PHONY: build-query-service-static-amd64 +build-query-service-static-amd64: + make GOARCH=amd64 build-query-service-static + +.PHONY: build-query-service-static-arm64 +build-query-service-static-arm64: + make CC=aarch64-linux-gnu-gcc GOARCH=arm64 build-query-service-static + +# Steps to build static binary of query service for all platforms +.PHONY: build-query-service-static-all +build-query-service-static-all: build-query-service-static-amd64 build-query-service-static-arm64 + # Steps to build and push docker image of query service -.PHONY: build-query-service-amd64 build-push-query-service +.PHONY: build-query-service-amd64 build-push-query-service # Step to build docker image of query service in amd64 (used in build pipeline) -build-query-service-amd64: +build-query-service-amd64: build-query-service-static-amd64 @echo "------------------" @echo "--> Building query-service docker image for amd64" @echo "------------------" @docker build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile \ - -t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \ - --build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="$(LD_FLAGS)" . + --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \ + --build-arg TARGETPLATFORM="linux/amd64" . # Step to build and push docker image of query in amd64 and arm64 (used in push pipeline) -build-push-query-service: +build-push-query-service: build-query-service-static-all @echo "------------------" @echo "--> Building and pushing query-service docker image" @echo "------------------" @docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plain \ - --push --platform linux/arm64,linux/amd64 --build-arg LD_FLAGS="$(LD_FLAGS)" \ + --push --platform linux/arm64,linux/amd64 \ --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) . # Step to build EE docker image of query service in amd64 (used in build pipeline) @@ -82,24 +123,14 @@ build-ee-query-service-amd64: @echo "------------------" @echo "--> Building query-service docker image for amd64" @echo "------------------" - @if [ $(DEV_BUILD) != "" ]; then \ - docker build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \ - -t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \ - --build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="${LD_FLAGS} ${DEV_LD_FLAGS}" .; \ - else \ - docker build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \ - -t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \ - --build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="$(LD_FLAGS)" .; \ - fi + make QUERY_SERVICE_DIRECTORY=${EE_QUERY_SERVICE_DIRECTORY} build-query-service-amd64 # Step to build and push EE docker image of query in amd64 and arm64 (used in push pipeline) build-push-ee-query-service: @echo "------------------" @echo "--> Building and pushing query-service docker image" @echo "------------------" - @docker buildx build --file $(EE_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) . + make QUERY_SERVICE_DIRECTORY=${EE_QUERY_SERVICE_DIRECTORY} build-push-query-service dev-setup: mkdir -p /var/lib/signoz @@ -110,7 +141,7 @@ dev-setup: @echo "------------------" run-local: - @LOCAL_GOOS=$(LOCAL_GOOS) LOCAL_GOARCH=$(LOCAL_GOARCH) docker-compose -f \ + @docker-compose -f \ $(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \ up --build -d @@ -153,3 +184,4 @@ test: go test ./pkg/query-service/formatter/... go test ./pkg/query-service/tests/integration/... go test ./pkg/query-service/rules/... + go test ./pkg/query-service/collectorsimulator/... diff --git a/README.zh-cn.md b/README.zh-cn.md index aaa89551bf..32b6328fcb 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -1,170 +1,225 @@ -

- SigNoz-logo +SigNoz-logo -

监视你的应用,并可排查已部署应用中的问题,这是一个开源的可替代DataDog、NewRelic的方案

+

监控你的应用,并且可排查已部署应用的问题,这是一个可替代 DataDog、NewRelic 的开源方案

- Downloads + Downloads GitHub issues tweet

-## +

+ 文档 • + 中文ReadMe • + 德文ReadMe • + 葡萄牙语ReadMe • + Slack 社区 • + Twitter +

-SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式追踪来增加软件技术栈的可见性。 +## -👉 你能看到一些性能指标,服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。 +SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可以使用 SigNoz 实现如下能力: -👉 通过准确的追踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。 +👉 在同一块面板上,可视化 Metrics, Traces 和 Logs 内容。 -👉 聚合trace数据来获得业务相关指标。 +👉 你可以关注服务的 p99 延迟和错误率, 包括外部 API 调用和个别的端点。 -![screenzy-1644432902955](https://user-images.githubusercontent.com/504541/153270713-1b2156e6-ec03-42de-975b-3c02b8ec1836.png) -
-![screenzy-1644432986784](https://user-images.githubusercontent.com/504541/153270725-0efb73b3-06ed-4207-bf13-9b7e2e17c4b8.png) -
-![screenzy-1647005040573](https://user-images.githubusercontent.com/504541/157875938-a3d57904-ea6d-4278-b929-bd1408d7f94c.png) +👉 你可以找到问题的根因,通过提取相关问题的 traces 日志、单独查看请求 traces 的火焰图详情。 + +👉 执行 trace 数据聚合,以获取业务相关的 metrics + +👉 对日志过滤和查询,通过日志的属性建立看板和告警 + +👉 通过 Python,java,Ruby 和 Javascript 自动记录异常 + +👉 轻松的自定义查询和设置告警 + +### 应用 Metrics 展示 + +![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png) + +### 分布式追踪 + +distributed_tracing_2 2 + +distributed_tracing_1 + +### 日志管理 + +logs_management + +### 基础设施监控 + +infrastructure_monitoring + +### 异常监控 + +![exceptions_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.png) + +### 告警 + +alerts_management

- +## 加入我们 Slack 社区 -## 加入我们的Slack社区 - -来[Slack](https://signoz.io/slack) 跟我们打声招呼👋 +来 [Slack](https://signoz.io/slack) 和我们打招呼吧 👋

- +## 特性: -## 功能: +- 为 metrics, traces and logs 制定统一的 UI。 无需切换 Prometheus 到 Jaeger 去查找问题,也无需使用想 Elastic 这样的日志工具分开你的 metrics 和 traces -- 应用概览指标(metrics),如RPS, p50/p90/p99延迟率分位值,错误率等。 -- 应用中最慢的终端(endpoint) -- 查看特定请求的trace数据来分析下游服务问题、慢数据库查询问题 及调用第三方服务如支付网关的问题 -- 通过服务名称、操作、延迟、错误、标签来过滤traces。 -- 聚合trace数据(events/spans)来得到业务相关指标。比如,你可以通过过滤条件`customer_type: gold` or `deployment_version: v2` or `external_call: paypal` 来获取指定业务的错误率和p99延迟 -- 为metrics和trace提供统一的UI。排查问题不需要在Prometheus和Jaeger之间切换。 +- 默认统计应用的 metrics 数据,像 RPS (每秒请求数), 50th/90th/99th 的分位数延迟数据,还有相关的错误率 + +- 找到应用中最慢的端点 + +- 查看准确的请求跟踪数据,找到下游服务的问题了,比如 DB 慢查询,或者调用第三方的支付网关等 + +- 通过 服务名、操作方式、延迟、错误、标签/注释 过滤 traces 数据 + +- 通过聚合 trace 数据而获得业务相关的 metrics。 比如你可以通过 `customer_type: gold` 或者 `deployment_version: v2` 或者 `external_call: paypal` 获取错误率和 P99 延迟数据 + +- 原生支持 OpenTelemetry 日志,高级日志查询,自动收集 k8s 相关日志 + +- 快如闪电的日志分析 ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/)) + +- 可视化点到点的基础设施性能,提取有所有类型机器的 metrics 数据 + +- 轻易自定义告警查询

- +## 为什么使用 SigNoz? -## 为何选择SigNoz? +作为开发者, 我们发现 SaaS 厂商对一些大家想要的小功能都是闭源的,这种行为真的让人有点恼火。 闭源厂商还会在月底给你一张没有明细的巨额账单。 -作为开发人员,我们发现依赖闭源的SaaS厂商提供的每个小功能有些麻烦,闭源厂商通常会给你一份巨额月付账单,但不提供足够的透明度,你不知道你为哪些功能付费。 +我们想做一个自托管并且可开源的工具,像 DataDog 和 NewRelic 那样, 为那些担心数据隐私和安全的公司提供第三方服务。 -我们想做一个自服务的开源版本的工具,类似于DataDog和NewRelic,用于那些对客户数据流入第三方有隐私和安全担忧的厂商。 +作为开源的项目,你完全可以自己掌控你的配置、样本和更新。你同样可以基于 SigNoz 拓展特定的业务模块。 -开源也让你对配置、采样和正常运行时间有完整的控制,你可以在SigNoz基础上构建模块来满足特定的商业需求。 +### 支持的编程语言: -### 语言支持 - -我们支持[OpenTelemetry](https://opentelemetry.io)库,你可以使用它来装备应用。也就是说SigNoz支持任何支持OpenTelemetry库的框架和语言。 主要支持语言包括: +我们支持 [OpenTelemetry](https://opentelemetry.io)。作为一个观测你应用的库文件。所以任何 OpenTelemetry 支持的框架和语言,对于 SigNoz 也同样支持。 一些主要支持的语言如下: - Java - Python - NodeJS - Go +- PHP +- .NET +- Ruby +- Elixir +- Rust -你可以在这个文档里找到完整的语言列表 - https://opentelemetry.io/docs/ +你可以在这里找到全部支持的语言列表 - https://opentelemetry.io/docs/

- +## 让我们开始吧 -## 入门 +### 使用 Docker 部署 +请一步步跟随 [这里](https://signoz.io/docs/install/docker/) 通过 docker 来安装。 -### 使用Docker部署 - -请按照[这里](https://signoz.io/docs/install/docker/)列出的步骤使用Docker来安装 - -如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/install/troubleshooting/)会对你有帮助。 +这个 [排障说明书](https://signoz.io/docs/install/troubleshooting/) 可以帮助你解决碰到的问题。

 

+### 使用 Helm 在 Kubernetes 部署 -### 使用Helm在Kubernetes上部署 - -请跟着[这里](https://signoz.io/docs/deployment/helm_chart)的步骤使用helm charts安装 +请一步步跟随 [这里](https://signoz.io/docs/deployment/helm_chart) 通过 helm 来安装

- - -## 与其他方案的比较 +## 比较相似的工具 ### SigNoz vs Prometheus -如果你只是需要监控指标(metrics),那Prometheus是不错的,但如果你要无缝的在metrics和traces之间切换,那目前把Prometheus & Jaeger串起来的体验并不好。 +Prometheus 是一个针对 metrics 监控的强大工具。但是如果你想无缝的切换 metrics 和 traces 查询,你当前大概率需要在 Prometheus 和 Jaeger 之间切换。 -我们的目标是为metrics和traces提供统一的UI - 类似于Datadog这样的Saas厂提供的方案。并且能够对trace进行过滤和聚合,这是目前Jaeger缺失的功能。 +我们的目标是提供一个客户观测 metrics 和 traces 整合的 UI。就像 SaaS 供应商 DataDog,它提供很多 jaeger 缺失的功能,比如针对 traces 过滤功能和聚合功能。

 

### SigNoz vs Jaeger -Jaeger只做分布式追踪(distributed tracing),SigNoz则支持metrics,traces,logs ,即可视化的三大支柱。 +Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metrics, traces 和 logs 所有的观测。 -并且SigNoz有一些Jaeger没有的高级功能: +而且, SigNoz 相较于 Jaeger 拥有更对的高级功能: -- Jaegar UI无法在traces或过滤的traces上展示metrics。 -- Jaeger不能对过滤的traces做聚合操作。例如,拥有tag为customer_type='premium'的所有请求的p99延迟。而这个功能在SigNoz这儿是很容易实现。 +- Jaegar UI 不能提供任何基于 traces 的 metrics 查询和过滤。 + +- Jaeger 不能针对过滤的 traces 做聚合。 比如, p99 延迟的请求有个标签是 customer_type='premium'。 而这些在 SigNoz 可以轻松做到。 + +

 

+ +### SigNoz vs Elastic + +- SigNoz 的日志管理是基于 ClickHouse 实现的,可以使日志的聚合更加高效,因为它是基于 OLAP 的数据仓储。 + +- 与 Elastic 相比,可以节省 50% 的资源成本 + +我们已经公布了 Elastic 和 SigNoz 的性能对比。 请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark) + +

 

+ +### SigNoz vs Loki + +- SigNoz 支持大容量高基数的聚合,但是 loki 是不支持的。 + +- SigNoz 支持索引的高基数查询,并且对索引没有数量限制,而 Loki 会在添加部分索引后到达最大上限。 + +- 相较于 SigNoz,Loki 在搜索大量数据下既困难又缓慢。 + +我们已经发布了基准测试对比 Loki 和 SigNoz 性能。请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)

- - ## 贡献 +我们 ❤️ 你的贡献,无论大小。 请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 再开始给 SigNoz 做贡献。 -我们 ❤️ 任何贡献无论大小。 请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 然后开始给Signoz做贡献。 +如果你不知道如何开始? 只需要在 [slack 社区](https://signoz.io/slack) 通过 `#contributing` 频道联系我们。 -还不清楚怎么开始? 只需在[slack社区](https://signoz.io/slack)的`#contributing`频道里ping我们。 +### 项目维护人员 -### Project maintainers - -#### Backend +#### 后端 - [Ankit Nayan](https://github.com/ankitnayan) - [Nityananda Gohain](https://github.com/nityanandagohain) - [Srikanth Chekuri](https://github.com/srikanthccv) - [Vishal Sharma](https://github.com/makeavish) -#### Frontend +#### 前端 - [Palash Gupta](https://github.com/palashgdev) -#### DevOps +#### 运维开发 - [Prashant Shahi](https://github.com/prashant-shahi)

- - ## 文档 -文档在这里:https://signoz.io/docs/. 如果你觉得有任何不清楚或者有文档缺失,请在Github里发一个问题,并使用标签 `documentation` 或者在社区stack频道里告诉我们。 +你可以通过 https://signoz.io/docs/ 找到相关文档。如果你需要阐述问题或者发现一些确实的事件, 通过标签为 `documentation` 提交 Github 问题。或者通过 slack 社区频道。

- - ## 社区 -加入[slack community](https://signoz.io/slack),了解更多关于分布式跟踪、可观察性(observability),以及SigNoz。同时与其他用户和贡献者一起交流。 +加入 [slack 社区](https://signoz.io/slack) 去了解更多关于分布式追踪、可观测性系统 。或者与 SigNoz 其他用户和贡献者交流。 -如果你有任何想法、问题或者反馈,请在[Github Discussions](https://github.com/SigNoz/signoz/discussions)分享给我们。 +如果你有任何想法、问题、或者任何反馈, 请通过 [Github Discussions](https://github.com/SigNoz/signoz/discussions) 分享。 -最后,感谢我们这些优秀的贡献者们。 +不管怎么样,感谢这个项目的所有贡献者! - - - diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index a9e778642e..73681aa72c 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -144,7 +144,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.30.0 + image: signoz/query-service:0.31.0 command: [ "-config=/root/config/prometheus.yml", @@ -184,7 +184,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:0.30.0 + image: signoz/frontend:0.31.0 deploy: restart_policy: condition: on-failure @@ -197,7 +197,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.79.7 + image: signoz/signoz-otel-collector:0.79.8 command: [ "--config=/etc/otel-collector-config.yaml", @@ -230,7 +230,7 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:0.79.7 + image: signoz/signoz-otel-collector:0.79.8 command: [ "--config=/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 b32dbe9c65..4912e3a1b5 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -48,7 +48,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: signoz-otel-collector - image: signoz/signoz-otel-collector:0.79.7 + image: signoz/signoz-otel-collector:0.79.8 command: [ "--config=/etc/otel-collector-config.yaml", @@ -78,7 +78,7 @@ services: otel-collector-metrics: container_name: signoz-otel-collector-metrics - image: signoz/signoz-otel-collector:0.79.7 + image: signoz/signoz-otel-collector:0.79.8 command: [ "--config=/etc/otel-collector-metrics-config.yaml", diff --git a/deploy/docker/clickhouse-setup/docker-compose-local.yaml b/deploy/docker/clickhouse-setup/docker-compose-local.yaml index 2c7b9a5c46..78aa72ff75 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-local.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-local.yaml @@ -8,7 +8,7 @@ services: dockerfile: "./Dockerfile" args: LDFLAGS: "" - TARGETPLATFORM: "${LOCAL_GOOS}/${LOCAL_GOARCH}" + TARGETPLATFORM: "${GOOS}/${GOARCH}" container_name: signoz-query-service environment: - ClickHouseUrl=tcp://clickhouse:9000 @@ -52,8 +52,8 @@ services: context: "../../../frontend" dockerfile: "./Dockerfile" args: - TARGETOS: "${LOCAL_GOOS}" - TARGETPLATFORM: "${LOCAL_GOARCH}" + TARGETOS: "${GOOS}" + TARGETPLATFORM: "${GOARCH}" container_name: signoz-frontend environment: - FRONTEND_API_ENDPOINT=http://query-service:8080 diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 7c770c5acf..c9f7be2769 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -162,7 +162,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.30.0} + image: signoz/query-service:${DOCKER_TAG:-0.31.0} container_name: signoz-query-service command: [ @@ -201,7 +201,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.30.0} + image: signoz/frontend:${DOCKER_TAG:-0.31.0} container_name: signoz-frontend restart: on-failure depends_on: @@ -213,7 +213,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.7} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.8} container_name: signoz-otel-collector command: [ @@ -244,7 +244,7 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.7} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.8} container_name: signoz-otel-collector-metrics command: [ diff --git a/ee/query-service/Dockerfile b/ee/query-service/Dockerfile index 258b0869f7..46b2186ec4 100644 --- a/ee/query-service/Dockerfile +++ b/ee/query-service/Dockerfile @@ -1,40 +1,20 @@ -FROM golang:1.21-bookworm AS builder - -# LD_FLAGS is passed as argument from Makefile. It will be empty, if no argument passed -ARG LD_FLAGS -ARG TARGETPLATFORM - -ENV CGO_ENABLED=1 -ENV GOPATH=/go - -RUN export GOOS=$(echo ${TARGETPLATFORM} | cut -d / -f1) && \ - export GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) - -# Prepare and enter src directory -WORKDIR /go/src/github.com/signoz/signoz - -# Add the sources and proceed with build -ADD . . -RUN cd ee/query-service \ - && go build -tags timetzdata -a -o ./bin/query-service \ - -ldflags "-linkmode external -extldflags '-static' -s -w $LD_FLAGS" \ - && chmod +x ./bin/query-service - - # use a minimal alpine image -FROM alpine:3.16.7 +FROM alpine:3.17 # Add Maintainer Info LABEL maintainer="signoz" +# define arguments that can be passed during build time +ARG TARGETOS TARGETARCH + # add ca-certificates in case you need them RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* # set working directory WORKDIR /root -# copy the binary from builder -COPY --from=builder /go/src/github.com/signoz/signoz/ee/query-service/bin/query-service . +# copy the query-service binary +COPY ee/query-service/bin/query-service-${TARGETOS}-${TARGETARCH} /root/query-service # copy prometheus YAML config COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml @@ -45,7 +25,6 @@ RUN chmod 755 /root /root/query-service # run the binary ENTRYPOINT ["./query-service"] -CMD ["-config", "../config/prometheus.yml"] -# CMD ["./query-service -config /root/config/prometheus.yml"] +CMD ["-config", "/root/config/prometheus.yml"] EXPOSE 8080 diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 5cdca8e204..1c0171efac 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -8,6 +8,7 @@ import ( "go.signoz.io/signoz/ee/query-service/dao" "go.signoz.io/signoz/ee/query-service/interfaces" "go.signoz.io/signoz/ee/query-service/license" + "go.signoz.io/signoz/ee/query-service/usage" baseapp "go.signoz.io/signoz/pkg/query-service/app" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/cache" @@ -27,6 +28,7 @@ type APIHandlerOptions struct { DialTimeout time.Duration AppDao dao.ModelDao RulesManager *rules.Manager + UsageManager *usage.Manager FeatureFlags baseint.FeatureLookup LicenseManager *license.Manager LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController @@ -82,6 +84,10 @@ func (ah *APIHandler) LM() *license.Manager { return ah.opts.LicenseManager } +func (ah *APIHandler) UM() *usage.Manager { + return ah.opts.UsageManager +} + func (ah *APIHandler) AppDao() dao.ModelDao { return ah.opts.AppDao } @@ -150,6 +156,13 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.getPATs)).Methods(http.MethodGet) router.HandleFunc("/api/v1/pat/{id}", am.OpenAccess(ah.deletePAT)).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) + + router.HandleFunc("/api/v2/licenses", + am.ViewAccess(ah.listLicensesV2)). + Methods(http.MethodGet) + ah.APIHandler.RegisterRoutes(router, am) } diff --git a/ee/query-service/app/api/license.go b/ee/query-service/app/api/license.go index e5f5b0ca0a..a24ba122d2 100644 --- a/ee/query-service/app/api/license.go +++ b/ee/query-service/app/api/license.go @@ -4,10 +4,44 @@ import ( "context" "encoding/json" "fmt" - "go.signoz.io/signoz/ee/query-service/model" + "io" "net/http" + + "go.signoz.io/signoz/ee/query-service/constants" + "go.signoz.io/signoz/ee/query-service/model" + "go.uber.org/zap" ) +type tierBreakdown struct { + UnitPrice float64 `json:"unitPrice"` + Quantity int64 `json:"quantity"` + TierStart int64 `json:"tierStart"` + TierEnd int64 `json:"tierEnd"` + TierCost float64 `json:"tierCost"` +} + +type usageResponse struct { + Type string `json:"type"` + Unit string `json:"unit"` + Tiers []tierBreakdown `json:"tiers"` +} + +type details struct { + Total float64 `json:"total"` + Breakdown []usageResponse `json:"breakdown"` + BaseFee float64 `json:"baseFee"` +} + +type billingDetails struct { + Status string `json:"status"` + Data struct { + BillingPeriodStart int64 `json:"billingPeriodStart"` + BillingPeriodEnd int64 `json:"billingPeriodEnd"` + Details details `json:"details"` + Discount float64 `json:"discount"` + } `json:"data"` +} + func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) { licenses, apiError := ah.LM().GetLicenses(context.Background()) if apiError != nil { @@ -38,3 +72,150 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) { ah.Respond(w, license) } + +func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) { + + type checkoutResponse struct { + Status string `json:"status"` + Data struct { + RedirectURL string `json:"redirectURL"` + } `json:"data"` + } + + hClient := &http.Client{} + req, err := http.NewRequest("POST", constants.LicenseSignozIo+"/checkout", r.Body) + if err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey) + licenseResp, err := hClient.Do(req) + if err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + + // decode response body + var resp checkoutResponse + if err := json.NewDecoder(licenseResp.Body).Decode(&resp); err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + + ah.Respond(w, resp.Data) +} + +func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) { + licenseKey := r.URL.Query().Get("licenseKey") + + if licenseKey == "" { + RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil) + return + } + + billingURL := fmt.Sprintf("%s/usage?licenseKey=%s", constants.LicenseSignozIo, licenseKey) + + hClient := &http.Client{} + req, err := http.NewRequest("GET", billingURL, nil) + if err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey) + billingResp, err := hClient.Do(req) + if err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + + // decode response body + var billingResponse billingDetails + if err := json.NewDecoder(billingResp.Body).Decode(&billingResponse); err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + + // TODO(srikanthccv):Fetch the current day usage and add it to the response + ah.Respond(w, billingResponse.Data) +} + +func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) { + + licenses, apiError := ah.LM().GetLicenses(context.Background()) + if apiError != nil { + RespondError(w, apiError, nil) + } + + resp := model.Licenses{ + TrialStart: -1, + TrialEnd: -1, + OnTrial: false, + WorkSpaceBlock: false, + Licenses: licenses, + } + + var currentActiveLicenseKey string + + for _, license := range licenses { + if license.IsCurrent { + currentActiveLicenseKey = license.Key + } + } + + // For the case when no license is applied i.e community edition + // There will be no trial details or license details + if currentActiveLicenseKey == "" { + ah.Respond(w, resp) + return + } + + // Fetch trial details + hClient := &http.Client{} + url := fmt.Sprintf("%s/trial?licenseKey=%s", constants.LicenseSignozIo, currentActiveLicenseKey) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + zap.S().Error("Error while creating request for trial details", err) + // If there is an error in fetching trial details, we will still return the license details + // to avoid blocking the UI + ah.Respond(w, resp) + return + } + req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey) + trialResp, err := hClient.Do(req) + if err != nil { + zap.S().Error("Error while fetching trial details", err) + // If there is an error in fetching trial details, we will still return the license details + // to avoid incorrectly blocking the UI + ah.Respond(w, resp) + return + } + defer trialResp.Body.Close() + + trialRespBody, err := io.ReadAll(trialResp.Body) + + if err != nil || trialResp.StatusCode != http.StatusOK { + zap.S().Error("Error while fetching trial details", err) + // If there is an error in fetching trial details, we will still return the license details + // to avoid incorrectly blocking the UI + ah.Respond(w, resp) + return + } + + // decode response body + var trialRespData model.SubscriptionServerResp + + if err := json.Unmarshal(trialRespBody, &trialRespData); err != nil { + zap.S().Error("Error while decoding trial details", err) + // If there is an error in fetching trial details, we will still return the license details + // to avoid incorrectly blocking the UI + ah.Respond(w, resp) + return + } + + resp.TrialStart = trialRespData.Data.TrialStart + resp.TrialEnd = trialRespData.Data.TrialEnd + resp.OnTrial = trialRespData.Data.OnTrial + resp.WorkSpaceBlock = trialRespData.Data.WorkSpaceBlock + + ah.Respond(w, resp) +} diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index e36d201ec3..834575643b 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -217,6 +217,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { DialTimeout: serverOptions.DialTimeout, AppDao: modelDao, RulesManager: rm, + UsageManager: usageManager, FeatureFlags: lm, LicenseManager: lm, LogsParsingPipelineController: logParsingPipelineController, diff --git a/ee/query-service/constants/constants.go b/ee/query-service/constants/constants.go index 45fad74da6..4953f4d3eb 100644 --- a/ee/query-service/constants/constants.go +++ b/ee/query-service/constants/constants.go @@ -9,6 +9,7 @@ const ( ) var LicenseSignozIo = "https://license.signoz.io/api/v1" +var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "") var SpanLimitStr = GetOrDefaultEnv("SPAN_LIMIT", "5000") diff --git a/ee/query-service/model/license.go b/ee/query-service/model/license.go index e1e6a997da..3ba89cf456 100644 --- a/ee/query-service/model/license.go +++ b/ee/query-service/model/license.go @@ -89,3 +89,16 @@ func (l *License) ParseFeatures() { l.FeatureSet = BasicPlan } } + +type Licenses struct { + TrialStart int64 `json:"trialStart"` + TrialEnd int64 `json:"trialEnd"` + OnTrial bool `json:"onTrial"` + WorkSpaceBlock bool `json:"workSpaceBlock"` + Licenses []License `json:"licenses"` +} + +type SubscriptionServerResp struct { + Status string `json:"status"` + Data Licenses `json:"data"` +} diff --git a/frontend/.dockerignore b/frontend/.dockerignore index a106e6a11e..840adcb19f 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1,4 +1,3 @@ node_modules .vscode -build .git diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 3209052799..ddbf9edc19 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,38 +1,17 @@ -# Builder stage -FROM node:16.15.0 as builder +FROM nginx:1.25.2-alpine # Add Maintainer Info LABEL maintainer="signoz" -ARG TARGETOS=linux -ARG TARGETARCH - +# Set working directory WORKDIR /frontend -# Copy the package.json and .yarnrc files prior to install dependencies -COPY package.json ./ -# Copy lock file -COPY yarn.lock ./ -COPY .yarnrc ./ - -# Install the dependencies and make the folder -RUN CI=1 yarn install - -COPY . . - -# Build the project and copy the files -RUN yarn build - - -FROM nginx:1.25.2-alpine - -COPY conf/default.conf /etc/nginx/conf.d/default.conf - # Remove default nginx index page RUN rm -rf /usr/share/nginx/html/* -# Copy from the stahg 1 -COPY --from=builder /frontend/build /usr/share/nginx/html +# Copy custom nginx config and static files +COPY conf/default.conf /etc/nginx/conf.d/default.conf +COPY build /usr/share/nginx/html EXPOSE 3301 diff --git a/frontend/public/locales/en-GB/dashboard.json b/frontend/public/locales/en-GB/dashboard.json index b643f4727d..b69113483d 100644 --- a/frontend/public/locales/en-GB/dashboard.json +++ b/frontend/public/locales/en-GB/dashboard.json @@ -13,5 +13,12 @@ "import_dashboard_by_pasting": "Import dashboard by pasting JSON or importing JSON file", "error_loading_json": "Error loading JSON file", "empty_json_not_allowed": "Empty JSON is not allowed", - "new_dashboard_title": "Sample Title" + "new_dashboard_title": "Sample Title", + "layout_saved_successfully": "Layout saved successfully", + "add_panel": "Add Panel", + "save_layout": "Save Layout", + "variable_updated_successfully": "Variable updated successfully", + "error_while_updating_variable": "Error while updating variable", + "dashboard_has_been_updated": "Dashboard has been updated", + "do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?" } diff --git a/frontend/public/locales/en-GB/routes.json b/frontend/public/locales/en-GB/routes.json index 5dd331eaf7..a3357435dd 100644 --- a/frontend/public/locales/en-GB/routes.json +++ b/frontend/public/locales/en-GB/routes.json @@ -1,13 +1,14 @@ -{ - "general": "General", - "alert_channels": "Alert Channels", - "organization_settings": "Organization Settings", - "my_settings": "My Settings", - "overview_metrics": "Overview Metrics", - "dbcall_metrics": "Database Calls", - "external_metrics": "External Calls", - "pipeline": "Pipeline", - "pipelines": "Pipelines", - "archives": "Archives", - "logs_to_metrics": "Logs To Metrics" -} +{ + "general": "General", + "alert_channels": "Alert Channels", + "organization_settings": "Organization Settings", + "ingestion_settings": "Ingestion Settings", + "my_settings": "My Settings", + "overview_metrics": "Overview Metrics", + "dbcall_metrics": "Database Calls", + "external_metrics": "External Calls", + "pipeline": "Pipeline", + "pipelines": "Pipelines", + "archives": "Archives", + "logs_to_metrics": "Logs To Metrics" +} diff --git a/frontend/public/locales/en-GB/titles.json b/frontend/public/locales/en-GB/titles.json index 8b078211e3..d61817e520 100644 --- a/frontend/public/locales/en-GB/titles.json +++ b/frontend/public/locales/en-GB/titles.json @@ -1,37 +1,38 @@ -{ - "SIGN_UP": "SigNoz | Sign Up", - "LOGIN": "SigNoz | Login", - "GET_STARTED": "SigNoz | Get Started", - "SERVICE_METRICS": "SigNoz | Service Metrics", - "SERVICE_MAP": "SigNoz | Service Map", - "TRACE": "SigNoz | Trace", - "TRACE_DETAIL": "SigNoz | Trace Detail", - "TRACES_EXPLORER": "SigNoz | Traces Explorer", - "SETTINGS": "SigNoz | Settings", - "USAGE_EXPLORER": "SigNoz | Usage Explorer", - "APPLICATION": "SigNoz | Home", - "ALL_DASHBOARD": "SigNoz | All Dashboards", - "DASHBOARD": "SigNoz | Dashboard", - "DASHBOARD_WIDGET": "SigNoz | Dashboard Widget", - "EDIT_ALERTS": "SigNoz | Edit Alerts", - "LIST_ALL_ALERT": "SigNoz | All Alerts", - "ALERTS_NEW": "SigNoz | New Alert", - "ALL_CHANNELS": "SigNoz | All Channels", - "CHANNELS_NEW": "SigNoz | New Channel", - "CHANNELS_EDIT": "SigNoz | Edit Channel", - "ALL_ERROR": "SigNoz | All Errors", - "ERROR_DETAIL": "SigNoz | Error Detail", - "VERSION": "SigNoz | Version", - "MY_SETTINGS": "SigNoz | My Settings", - "ORG_SETTINGS": "SigNoz | Organization Settings", - "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", - "UN_AUTHORIZED": "SigNoz | Unauthorized", - "NOT_FOUND": "SigNoz | Page Not Found", - "LOGS": "SigNoz | Logs", - "LOGS_EXPLORER": "SigNoz | Logs Explorer", - "LIVE_LOGS": "SigNoz | Live Logs", - "HOME_PAGE": "Open source Observability Platform | SigNoz", - "PASSWORD_RESET": "SigNoz | Password Reset", - "LIST_LICENSES": "SigNoz | List of Licenses", - "DEFAULT": "Open source Observability Platform | SigNoz" -} +{ + "SIGN_UP": "SigNoz | Sign Up", + "LOGIN": "SigNoz | Login", + "GET_STARTED": "SigNoz | Get Started", + "SERVICE_METRICS": "SigNoz | Service Metrics", + "SERVICE_MAP": "SigNoz | Service Map", + "TRACE": "SigNoz | Trace", + "TRACE_DETAIL": "SigNoz | Trace Detail", + "TRACES_EXPLORER": "SigNoz | Traces Explorer", + "SETTINGS": "SigNoz | Settings", + "USAGE_EXPLORER": "SigNoz | Usage Explorer", + "APPLICATION": "SigNoz | Home", + "ALL_DASHBOARD": "SigNoz | All Dashboards", + "DASHBOARD": "SigNoz | Dashboard", + "DASHBOARD_WIDGET": "SigNoz | Dashboard Widget", + "EDIT_ALERTS": "SigNoz | Edit Alerts", + "LIST_ALL_ALERT": "SigNoz | All Alerts", + "ALERTS_NEW": "SigNoz | New Alert", + "ALL_CHANNELS": "SigNoz | All Channels", + "CHANNELS_NEW": "SigNoz | New Channel", + "CHANNELS_EDIT": "SigNoz | Edit Channel", + "ALL_ERROR": "SigNoz | All Errors", + "ERROR_DETAIL": "SigNoz | Error Detail", + "VERSION": "SigNoz | Version", + "MY_SETTINGS": "SigNoz | My Settings", + "ORG_SETTINGS": "SigNoz | Organization Settings", + "INGESTION_SETTINGS": "SigNoz | Ingestion Settings", + "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", + "UN_AUTHORIZED": "SigNoz | Unauthorized", + "NOT_FOUND": "SigNoz | Page Not Found", + "LOGS": "SigNoz | Logs", + "LOGS_EXPLORER": "SigNoz | Logs Explorer", + "LIVE_LOGS": "SigNoz | Live Logs", + "HOME_PAGE": "Open source Observability Platform | SigNoz", + "PASSWORD_RESET": "SigNoz | Password Reset", + "LIST_LICENSES": "SigNoz | List of Licenses", + "DEFAULT": "Open source Observability Platform | SigNoz" +} diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index b643f4727d..b69113483d 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -13,5 +13,12 @@ "import_dashboard_by_pasting": "Import dashboard by pasting JSON or importing JSON file", "error_loading_json": "Error loading JSON file", "empty_json_not_allowed": "Empty JSON is not allowed", - "new_dashboard_title": "Sample Title" + "new_dashboard_title": "Sample Title", + "layout_saved_successfully": "Layout saved successfully", + "add_panel": "Add Panel", + "save_layout": "Save Layout", + "variable_updated_successfully": "Variable updated successfully", + "error_while_updating_variable": "Error while updating variable", + "dashboard_has_been_updated": "Dashboard has been updated", + "do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?" } diff --git a/frontend/public/locales/en/routes.json b/frontend/public/locales/en/routes.json index 5dd331eaf7..a3357435dd 100644 --- a/frontend/public/locales/en/routes.json +++ b/frontend/public/locales/en/routes.json @@ -1,13 +1,14 @@ -{ - "general": "General", - "alert_channels": "Alert Channels", - "organization_settings": "Organization Settings", - "my_settings": "My Settings", - "overview_metrics": "Overview Metrics", - "dbcall_metrics": "Database Calls", - "external_metrics": "External Calls", - "pipeline": "Pipeline", - "pipelines": "Pipelines", - "archives": "Archives", - "logs_to_metrics": "Logs To Metrics" -} +{ + "general": "General", + "alert_channels": "Alert Channels", + "organization_settings": "Organization Settings", + "ingestion_settings": "Ingestion Settings", + "my_settings": "My Settings", + "overview_metrics": "Overview Metrics", + "dbcall_metrics": "Database Calls", + "external_metrics": "External Calls", + "pipeline": "Pipeline", + "pipelines": "Pipelines", + "archives": "Archives", + "logs_to_metrics": "Logs To Metrics" +} diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index 2a1036dc57..26e2141d38 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -1,37 +1,38 @@ -{ - "SIGN_UP": "SigNoz | Sign Up", - "LOGIN": "SigNoz | Login", - "SERVICE_METRICS": "SigNoz | Service Metrics", - "SERVICE_MAP": "SigNoz | Service Map", - "GET_STARTED": "SigNoz | Get Started", - "TRACE": "SigNoz | Trace", - "TRACE_DETAIL": "SigNoz | Trace Detail", - "TRACES_EXPLORER": "SigNoz | Traces Explorer", - "SETTINGS": "SigNoz | Settings", - "USAGE_EXPLORER": "SigNoz | Usage Explorer", - "APPLICATION": "SigNoz | Home", - "ALL_DASHBOARD": "SigNoz | All Dashboards", - "DASHBOARD": "SigNoz | Dashboard", - "DASHBOARD_WIDGET": "SigNoz | Dashboard Widget", - "EDIT_ALERTS": "SigNoz | Edit Alerts", - "LIST_ALL_ALERT": "SigNoz | All Alerts", - "ALERTS_NEW": "SigNoz | New Alert", - "ALL_CHANNELS": "SigNoz | All Channels", - "CHANNELS_NEW": "SigNoz | New Channel", - "CHANNELS_EDIT": "SigNoz | Edit Channel", - "ALL_ERROR": "SigNoz | All Errors", - "ERROR_DETAIL": "SigNoz | Error Detail", - "VERSION": "SigNoz | Version", - "MY_SETTINGS": "SigNoz | My Settings", - "ORG_SETTINGS": "SigNoz | Organization Settings", - "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", - "UN_AUTHORIZED": "SigNoz | Unauthorized", - "NOT_FOUND": "SigNoz | Page Not Found", - "LOGS": "SigNoz | Logs", - "LOGS_EXPLORER": "SigNoz | Logs Explorer", - "LIVE_LOGS": "SigNoz | Live Logs", - "HOME_PAGE": "Open source Observability Platform | SigNoz", - "PASSWORD_RESET": "SigNoz | Password Reset", - "LIST_LICENSES": "SigNoz | List of Licenses", - "DEFAULT": "Open source Observability Platform | SigNoz" -} +{ + "SIGN_UP": "SigNoz | Sign Up", + "LOGIN": "SigNoz | Login", + "SERVICE_METRICS": "SigNoz | Service Metrics", + "SERVICE_MAP": "SigNoz | Service Map", + "GET_STARTED": "SigNoz | Get Started", + "TRACE": "SigNoz | Trace", + "TRACE_DETAIL": "SigNoz | Trace Detail", + "TRACES_EXPLORER": "SigNoz | Traces Explorer", + "SETTINGS": "SigNoz | Settings", + "USAGE_EXPLORER": "SigNoz | Usage Explorer", + "APPLICATION": "SigNoz | Home", + "ALL_DASHBOARD": "SigNoz | All Dashboards", + "DASHBOARD": "SigNoz | Dashboard", + "DASHBOARD_WIDGET": "SigNoz | Dashboard Widget", + "EDIT_ALERTS": "SigNoz | Edit Alerts", + "LIST_ALL_ALERT": "SigNoz | All Alerts", + "ALERTS_NEW": "SigNoz | New Alert", + "ALL_CHANNELS": "SigNoz | All Channels", + "CHANNELS_NEW": "SigNoz | New Channel", + "CHANNELS_EDIT": "SigNoz | Edit Channel", + "ALL_ERROR": "SigNoz | All Errors", + "ERROR_DETAIL": "SigNoz | Error Detail", + "VERSION": "SigNoz | Version", + "MY_SETTINGS": "SigNoz | My Settings", + "ORG_SETTINGS": "SigNoz | Organization Settings", + "INGESTION_SETTINGS": "SigNoz | Ingestion Settings", + "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", + "UN_AUTHORIZED": "SigNoz | Unauthorized", + "NOT_FOUND": "SigNoz | Page Not Found", + "LOGS": "SigNoz | Logs", + "LOGS_EXPLORER": "SigNoz | Logs Explorer", + "LIVE_LOGS": "SigNoz | Live Logs", + "HOME_PAGE": "Open source Observability Platform | SigNoz", + "PASSWORD_RESET": "SigNoz | Password Reset", + "LIST_LICENSES": "SigNoz | List of Licenses", + "DEFAULT": "Open source Observability Platform | SigNoz" +} diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 56ba19fdf4..c2a0db3da1 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -12,6 +12,7 @@ import useGetFeatureFlag from 'hooks/useGetFeatureFlag'; import { NotificationProvider } from 'hooks/useNotifications'; import { ResourceProvider } from 'hooks/useResourceAttribute'; import history from 'lib/history'; +import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { QueryBuilderProvider } from 'providers/QueryBuilder'; import { Suspense, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -110,22 +111,24 @@ function App(): JSX.Element { - - }> - - {routes.map(({ path, component, exact }) => ( - - ))} + + + }> + + {routes.map(({ path, component, exact }) => ( + + ))} - - - - + + + + + diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index e13282542f..3852153da8 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -102,6 +102,10 @@ export const OrganizationSettings = Loadable( () => import(/* webpackChunkName: "All Settings" */ 'pages/Settings'), ); +export const IngestionSettings = Loadable( + () => import(/* webpackChunkName: "Ingestion Settings" */ 'pages/Settings'), +); + export const MySettings = Loadable( () => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'), ); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 36081197e5..0c0f5ae9cb 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -11,6 +11,7 @@ import { EditAlertChannelsAlerts, EditRulesPage, ErrorDetails, + IngestionSettings, LicensePage, ListAllALertsPage, LiveLogs, @@ -214,6 +215,13 @@ const routes: AppRoutes[] = [ isPrivate: true, key: 'ORG_SETTINGS', }, + { + path: ROUTES.INGESTION_SETTINGS, + exact: true, + component: IngestionSettings, + isPrivate: true, + key: 'INGESTION_SETTINGS', + }, { path: ROUTES.MY_SETTINGS, exact: true, diff --git a/frontend/src/api/dashboard/delete.ts b/frontend/src/api/dashboard/delete.ts index 2f7c8f16b9..8faf711383 100644 --- a/frontend/src/api/dashboard/delete.ts +++ b/frontend/src/api/dashboard/delete.ts @@ -1,24 +1,9 @@ import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { Props } from 'types/api/dashboard/delete'; +import { PayloadProps, Props } from 'types/api/dashboard/delete'; -const deleteDashboard = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.delete(`/dashboards/${props.uuid}`); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; +const deleteDashboard = (props: Props): Promise => + axios + .delete(`/dashboards/${props.uuid}`) + .then((response) => response.data); export default deleteDashboard; diff --git a/frontend/src/api/dashboard/get.ts b/frontend/src/api/dashboard/get.ts index 6c9c953e7d..9b10f6467d 100644 --- a/frontend/src/api/dashboard/get.ts +++ b/frontend/src/api/dashboard/get.ts @@ -1,24 +1,11 @@ import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/dashboard/get'; +import { ApiResponse } from 'types/api'; +import { Props } from 'types/api/dashboard/get'; +import { Dashboard } from 'types/api/dashboard/getAll'; -const get = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.get(`/dashboards/${props.uuid}`); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; +const get = (props: Props): Promise => + axios + .get>(`/dashboards/${props.uuid}`) + .then((res) => res.data.data); export default get; diff --git a/frontend/src/api/pipeline/preview.ts b/frontend/src/api/pipeline/preview.ts new file mode 100644 index 0000000000..a349ecdfec --- /dev/null +++ b/frontend/src/api/pipeline/preview.ts @@ -0,0 +1,21 @@ +import axios from 'api'; +import { ILog } from 'types/api/logs/log'; +import { PipelineData } from 'types/api/pipeline/def'; + +export interface PipelineSimulationRequest { + logs: ILog[]; + pipelines: PipelineData[]; +} + +export interface PipelineSimulationResponse { + logs: ILog[]; +} + +const simulatePipelineProcessing = async ( + requestBody: PipelineSimulationRequest, +): Promise => + axios + .post('/logs/pipelines/preview', requestBody) + .then((res) => res.data.data); + +export default simulatePipelineProcessing; diff --git a/frontend/src/api/settings/getIngestionData.ts b/frontend/src/api/settings/getIngestionData.ts new file mode 100644 index 0000000000..c35b964e28 --- /dev/null +++ b/frontend/src/api/settings/getIngestionData.ts @@ -0,0 +1,24 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { IngestionResponseProps } from 'types/api/settings/ingestion'; + +const getIngestionData = async (): Promise< + SuccessResponse | ErrorResponse +> => { + try { + const response = await axios.get(`/settings/ingestion_key`); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getIngestionData; diff --git a/frontend/src/components/MarkdownRenderer/MarkdownRenderer.tsx b/frontend/src/components/MarkdownRenderer/MarkdownRenderer.tsx index 82b438dcd0..cd6a5fdc33 100644 --- a/frontend/src/components/MarkdownRenderer/MarkdownRenderer.tsx +++ b/frontend/src/components/MarkdownRenderer/MarkdownRenderer.tsx @@ -1,10 +1,18 @@ +/* eslint-disable no-restricted-syntax */ /* eslint-disable react/jsx-props-no-spreading */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import ReactMarkdown from 'react-markdown'; import { CodeProps } from 'react-markdown/lib/ast-to-react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn'; +interface LinkProps { + href: string; + children: React.ReactElement; +} + function Pre({ children }: { children: React.ReactNode }): JSX.Element { return (
@@ -40,4 +48,54 @@ function Code({
 	);
 }
 
-export { Code, Pre };
+function Link({ href, children }: LinkProps): JSX.Element {
+	return (
+		
+			{children}
+		
+	);
+}
+
+const interpolateMarkdown = (
+	markdownContent: any,
+	variables: { [s: string]: unknown } | ArrayLike,
+) => {
+	let interpolatedContent = markdownContent;
+
+	const variableEntries = Object.entries(variables);
+
+	// Loop through variables and replace placeholders with values
+	for (const [key, value] of variableEntries) {
+		const placeholder = `{{${key}}}`;
+		const regex = new RegExp(placeholder, 'g');
+		interpolatedContent = interpolatedContent.replace(regex, value);
+	}
+
+	return interpolatedContent;
+};
+
+function MarkdownRenderer({
+	markdownContent,
+	variables,
+}: {
+	markdownContent: any;
+	variables: any;
+}): JSX.Element {
+	const interpolatedMarkdown = interpolateMarkdown(markdownContent, variables);
+
+	return (
+		
+			{interpolatedMarkdown}
+		
+	);
+}
+
+export { Code, Link, MarkdownRenderer, Pre };
diff --git a/frontend/src/components/TextToolTip/TextToolTip.style.scss b/frontend/src/components/TextToolTip/TextToolTip.style.scss
new file mode 100644
index 0000000000..192d98264c
--- /dev/null
+++ b/frontend/src/components/TextToolTip/TextToolTip.style.scss
@@ -0,0 +1,3 @@
+.overlay--text-wrap {
+    white-space: pre-wrap;
+}
\ No newline at end of file
diff --git a/frontend/src/components/TextToolTip/TextToolTip.tsx b/frontend/src/components/TextToolTip/TextToolTip.tsx
new file mode 100644
index 0000000000..6c8fad783e
--- /dev/null
+++ b/frontend/src/components/TextToolTip/TextToolTip.tsx
@@ -0,0 +1,89 @@
+import './TextToolTip.style.scss';
+
+import { blue, grey } from '@ant-design/colors';
+import {
+	QuestionCircleFilled,
+	QuestionCircleOutlined,
+} from '@ant-design/icons';
+import { Tooltip } from 'antd';
+import { themeColors } from 'constants/theme';
+import { useIsDarkMode } from 'hooks/useDarkMode';
+import { useMemo } from 'react';
+import { popupContainer } from 'utils/selectPopupContainer';
+
+import { style } from './constant';
+
+function TextToolTip({
+	text,
+	url,
+	useFilledIcon = true,
+	urlText,
+}: TextToolTipProps): JSX.Element {
+	const isDarkMode = useIsDarkMode();
+
+	const onClickHandler = (
+		event: React.MouseEvent,
+	): void => {
+		event.stopPropagation();
+	};
+
+	const overlay = useMemo(
+		() => (
+			
+ {`${text} `} + {url && ( + + {urlText || 'here'} + + )} +
+ ), + [text, url, urlText], + ); + + const iconStyle = useMemo( + () => ({ + ...style, + color: isDarkMode ? themeColors.whiteCream : grey[0], + }), + [isDarkMode], + ); + + const iconOutlinedStyle = useMemo( + () => ({ + ...style, + color: isDarkMode ? themeColors.navyBlue : blue[6], + }), + [isDarkMode], + ); + + return ( + + {useFilledIcon ? ( + + ) : ( + + )} + + ); +} + +TextToolTip.defaultProps = { + url: '', + urlText: '', + useFilledIcon: true, +}; +interface TextToolTipProps { + url?: string; + text: string; + useFilledIcon?: boolean; + urlText?: string; +} + +export default TextToolTip; diff --git a/frontend/src/components/TextToolTip/styles.ts b/frontend/src/components/TextToolTip/constant.ts similarity index 100% rename from frontend/src/components/TextToolTip/styles.ts rename to frontend/src/components/TextToolTip/constant.ts diff --git a/frontend/src/components/TextToolTip/index.tsx b/frontend/src/components/TextToolTip/index.tsx index 72f631a872..c40a841fd0 100644 --- a/frontend/src/components/TextToolTip/index.tsx +++ b/frontend/src/components/TextToolTip/index.tsx @@ -1,87 +1,3 @@ -import { blue, grey } from '@ant-design/colors'; -import { - QuestionCircleFilled, - QuestionCircleOutlined, -} from '@ant-design/icons'; -import { Tooltip } from 'antd'; -import { themeColors } from 'constants/theme'; -import { useIsDarkMode } from 'hooks/useDarkMode'; -import { useMemo } from 'react'; -import { popupContainer } from 'utils/selectPopupContainer'; - -import { style } from './styles'; - -function TextToolTip({ - text, - url, - useFilledIcon = true, - urlText, -}: TextToolTipProps): JSX.Element { - const isDarkMode = useIsDarkMode(); - - const onClickHandler = ( - event: React.MouseEvent, - ): void => { - event.stopPropagation(); - }; - - const overlay = useMemo( - () => ( -
- {`${text} `} - {url && ( - - {urlText || 'here'} - - )} -
- ), - [text, url, urlText], - ); - - const iconStyle = useMemo( - () => ({ - ...style, - color: isDarkMode ? themeColors.whiteCream : grey[0], - }), - [isDarkMode], - ); - - const iconOutlinedStyle = useMemo( - () => ({ - ...style, - color: isDarkMode ? themeColors.navyBlue : blue[6], - }), - [isDarkMode], - ); - - return ( - - {useFilledIcon ? ( - - ) : ( - - )} - - ); -} - -TextToolTip.defaultProps = { - url: '', - urlText: '', - useFilledIcon: true, -}; -interface TextToolTipProps { - url?: string; - text: string; - useFilledIcon?: boolean; - urlText?: string; -} +import TextToolTip from './TextToolTip'; export default TextToolTip; diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 1f984ebd46..ec55889516 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -3,5 +3,8 @@ export const REACT_QUERY_KEY = { GET_QUERY_RANGE: 'GET_QUERY_RANGE', GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS', GET_TRIGGERED_ALERTS: 'GET_TRIGGERED_ALERTS', + DASHBOARD_BY_ID: 'DASHBOARD_BY_ID', GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS', + DELETE_DASHBOARD: 'DELETE_DASHBOARD', + LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW', }; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index c910d0d25e..b156036ce4 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -24,6 +24,7 @@ const ROUTES = { VERSION: '/status', MY_SETTINGS: '/my-settings', ORG_SETTINGS: '/settings/org-settings', + INGESTION_SETTINGS: '/settings/ingestion-settings', SOMETHING_WENT_WRONG: '/something-went-wrong', UN_AUTHORIZED: '/un-authorized', NOT_FOUND: '/not-found', diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 2ac2f3a7b8..8517d9b18c 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -3,6 +3,7 @@ import { initialQueryPromQLData, PANEL_TYPES, } from 'constants/queryBuilder'; +import ROUTES from 'constants/routes'; import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertDef, @@ -77,7 +78,7 @@ export const logAlertDefaults: AlertDef = { }, labels: { severity: 'warning', - details: `${window.location.protocol}//${window.location.host}/logs`, + details: `${window.location.protocol}//${window.location.host}${ROUTES.LOGS_EXPLORER}`, }, annotations: defaultAnnotations, evalWindow: defaultEvalWindow, diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index f6bf35cbd7..c00b78b449 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -25,6 +25,7 @@ export interface ChartPreviewProps { headline?: JSX.Element; alertDef?: AlertDef; userQueryKey?: string; + allowSelectedIntervalForStepGen?: boolean; } function ChartPreview({ @@ -35,6 +36,7 @@ function ChartPreview({ selectedInterval = '5min', headline, userQueryKey, + allowSelectedIntervalForStepGen = false, alertDef, }: ChartPreviewProps): JSX.Element | null { const { t } = useTranslation('alerts'); @@ -89,6 +91,9 @@ function ChartPreview({ globalSelectedInterval: selectedInterval, graphType, selectedTime, + params: { + allowSelectedIntervalForStepGen, + }, }, { queryKey: [ @@ -127,7 +132,7 @@ function ChartPreview({ ); + const updatedStagedQuery = useMemo((): Query | null => { + const newQuery: Query | null = stagedQuery; + if (newQuery) { + newQuery.builder.queryData[0].stepInterval = getUpdatedStepInterval( + alertDef.evalWindow, + ); + } + return newQuery; + }, [alertDef.evalWindow, stagedQuery]); + const renderQBChartPreview = (): JSX.Element => ( } name="" - query={stagedQuery} + query={updatedStagedQuery} selectedInterval={toChartInterval(alertDef.evalWindow)} alertDef={alertDef} + allowSelectedIntervalForStepGen /> ); diff --git a/frontend/src/container/FormAlertRules/utils.test.ts b/frontend/src/container/FormAlertRules/utils.test.ts new file mode 100644 index 0000000000..49acf94bc1 --- /dev/null +++ b/frontend/src/container/FormAlertRules/utils.test.ts @@ -0,0 +1,14 @@ +// Write a test for getUpdatedStepInterval function in src/container/FormAlertRules/utils.ts + +import { getUpdatedStepInterval } from './utils'; + +describe('getUpdatedStepInterval', () => { + it('should return 60', () => { + const result = getUpdatedStepInterval('5m0s'); + expect(result).toEqual(60); + }); + it('should return 60 for 10m0s', () => { + const result = getUpdatedStepInterval('10m0s'); + expect(result).toEqual(60); + }); +}); diff --git a/frontend/src/container/FormAlertRules/utils.ts b/frontend/src/container/FormAlertRules/utils.ts index 67042569a0..3734474c29 100644 --- a/frontend/src/container/FormAlertRules/utils.ts +++ b/frontend/src/container/FormAlertRules/utils.ts @@ -1,4 +1,6 @@ import { Time } from 'container/TopNav/DateTimeSelection/config'; +import getStartEndRangeTime from 'lib/getStartEndRangeTime'; +import getStep from 'lib/getStep'; // toChartInterval converts eval window to chart selection time interval export const toChartInterval = (evalWindow: string | undefined): Time => { @@ -21,3 +23,15 @@ export const toChartInterval = (evalWindow: string | undefined): Time => { return '5min'; } }; + +export const getUpdatedStepInterval = (evalWindow?: string): number => { + const { start, end } = getStartEndRangeTime({ + type: 'GLOBAL_TIME', + interval: toChartInterval(evalWindow), + }); + return getStep({ + start, + end, + inputFormat: 'ns', + }); +}; diff --git a/frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx b/frontend/src/container/GridCardLayout/EmptyWidget/index.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx rename to frontend/src/container/GridCardLayout/EmptyWidget/index.tsx diff --git a/frontend/src/container/GridGraphLayout/EmptyWidget/styles.ts b/frontend/src/container/GridCardLayout/EmptyWidget/styles.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/EmptyWidget/styles.ts rename to frontend/src/container/GridCardLayout/EmptyWidget/styles.ts diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.styles.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.styles.scss new file mode 100644 index 0000000000..2d594aa8a9 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.styles.scss @@ -0,0 +1,21 @@ +.graph-manager-container { + margin-top: 1.25rem; + display: flex; + align-items: flex-end; + overflow-x: scroll; + + .filter-table-container { + flex-basis: 80%; + } + + .save-cancel-container { + flex-basis: 20%; + display: flex; + justify-content: flex-end; + } + + .save-cancel-button { + margin: 0 0.313rem; + } + +} \ No newline at end of file diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx new file mode 100644 index 0000000000..e6c6e26c0b --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx @@ -0,0 +1,136 @@ +import './GraphManager.styles.scss'; + +import { Button, Input } from 'antd'; +import { CheckboxChangeEvent } from 'antd/es/checkbox'; +import { ResizeTable } from 'components/ResizeTable'; +import { useNotifications } from 'hooks/useNotifications'; +import { memo, useCallback, useState } from 'react'; + +import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns'; +import { ExtendedChartDataset, GraphManagerProps } from './types'; +import { + getDefaultTableDataSet, + saveLegendEntriesToLocalStorage, +} from './utils'; + +function GraphManager({ + data, + name, + yAxisUnit, + onToggleModelHandler, + setGraphsVisibilityStates, + graphsVisibilityStates = [], + lineChartRef, + parentChartRef, +}: GraphManagerProps): JSX.Element { + const [tableDataSet, setTableDataSet] = useState( + getDefaultTableDataSet(data), + ); + + const { notifications } = useNotifications(); + + const checkBoxOnChangeHandler = useCallback( + (e: CheckboxChangeEvent, index: number): void => { + const newStates = [...graphsVisibilityStates]; + + newStates[index] = e.target.checked; + + lineChartRef?.current?.toggleGraph(index, e.target.checked); + + setGraphsVisibilityStates([...newStates]); + }, + [graphsVisibilityStates, setGraphsVisibilityStates, lineChartRef], + ); + + const labelClickedHandler = useCallback( + (labelIndex: number): void => { + const newGraphVisibilityStates = Array(data.datasets.length).fill( + false, + ); + newGraphVisibilityStates[labelIndex] = true; + + newGraphVisibilityStates.forEach((state, index) => { + lineChartRef?.current?.toggleGraph(index, state); + parentChartRef?.current?.toggleGraph(index, state); + }); + setGraphsVisibilityStates(newGraphVisibilityStates); + }, + [ + data.datasets.length, + setGraphsVisibilityStates, + lineChartRef, + parentChartRef, + ], + ); + + const columns = getGraphManagerTableColumns({ + data, + checkBoxOnChangeHandler, + graphVisibilityState: graphsVisibilityStates || [], + labelClickedHandler, + yAxisUnit, + }); + + const filterHandler = useCallback( + (event: React.ChangeEvent): void => { + const value = event.target.value.toString().toLowerCase(); + const updatedDataSet = tableDataSet.map((item) => { + if (item.label?.toLocaleLowerCase().includes(value)) { + return { ...item, show: true }; + } + return { ...item, show: false }; + }); + setTableDataSet(updatedDataSet); + }, + [tableDataSet], + ); + + const saveHandler = useCallback((): void => { + saveLegendEntriesToLocalStorage({ + data, + graphVisibilityState: graphsVisibilityStates || [], + name, + }); + notifications.success({ + message: 'The updated graphs & legends are saved', + }); + if (onToggleModelHandler) { + onToggleModelHandler(); + } + }, [data, graphsVisibilityStates, name, notifications, onToggleModelHandler]); + + const dataSource = tableDataSet.filter((item) => item.show); + + return ( +
+
+ + +
+
+ + + + + + +
+
+ ); +} + +GraphManager.defaultProps = { + graphVisibilityStateHandler: undefined, +}; + +export default memo(GraphManager); diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/CustomCheckBox.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx similarity index 59% rename from frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/CustomCheckBox.tsx rename to frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx index 22ae630bb8..eda971c1e4 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/CustomCheckBox.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx @@ -1,3 +1,4 @@ +import { grey } from '@ant-design/colors'; import { Checkbox, ConfigProvider } from 'antd'; import { CheckboxChangeEvent } from 'antd/es/checkbox'; @@ -6,7 +7,7 @@ import { CheckBoxProps } from '../types'; function CustomCheckBox({ data, index, - graphVisibilityState, + graphVisibilityState = [], checkBoxOnChangeHandler, }: CheckBoxProps): JSX.Element { const { datasets } = data; @@ -15,17 +16,21 @@ function CustomCheckBox({ checkBoxOnChangeHandler(e, index); }; + const color = datasets[index]?.borderColor?.toString() || grey[0]; + + const isChecked = graphVisibilityState[index] || false; + return ( - + ); } diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetLabel.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetLabel.tsx rename to frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GraphManagerColumns.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns.tsx similarity index 87% rename from frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GraphManagerColumns.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns.tsx index cc10f83f00..3702a9b3e0 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GraphManagerColumns.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns.tsx @@ -5,7 +5,7 @@ import { ChartData } from 'chart.js'; import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants'; import { DataSetProps } from '../types'; import { getGraphManagerTableHeaderTitle } from '../utils'; -import { getCheckBox } from './GetCheckBox'; +import CustomCheckBox from './CustomCheckBox'; import { getLabel } from './GetLabel'; export const getGraphManagerTableColumns = ({ @@ -20,11 +20,14 @@ export const getGraphManagerTableColumns = ({ width: 50, dataIndex: ColumnsKeyAndDataIndex.Index, key: ColumnsKeyAndDataIndex.Index, - ...getCheckBox({ - checkBoxOnChangeHandler, - graphVisibilityState, - data, - }), + render: (_: string, __: DataSetProps, index: number): JSX.Element => ( + + ), }, { title: ColumnsTitle[ColumnsKeyAndDataIndex.Label], diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/Label.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/Label.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/Label.tsx rename to frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/Label.tsx diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/contants.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/FullView/contants.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx similarity index 80% rename from frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx rename to frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index 1437a29157..02391bdc0c 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -12,17 +12,16 @@ import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { useChartMutable } from 'hooks/useChartMutable'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import getChartData from 'lib/getChartData'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { GlobalReducer } from 'types/reducer/globalTime'; -import { toggleGraphsVisibilityInChart } from '../utils'; import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants'; import GraphManager from './GraphManager'; import { GraphContainer, TimeContainer } from './styles'; import { FullViewProps } from './types'; -import { getIsGraphLegendToggleAvailable } from './utils'; function FullView({ widget, @@ -34,45 +33,29 @@ function FullView({ isDependedDataLoaded = false, graphsVisibilityStates, onToggleModelHandler, + setGraphsVisibilityStates, + parentChartRef, }: FullViewProps): JSX.Element { const { selectedTime: globalSelectedTime } = useSelector< AppState, GlobalReducer >((state) => state.globalTime); + const { selectedDashboard } = useDashboard(); + const getSelectedTime = useCallback( () => timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')), [widget], ); - const canModifyChart = useChartMutable({ - panelType: widget.panelTypes, - panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE, - }); - const lineChartRef = useRef(); - useEffect(() => { - if (graphsVisibilityStates && canModifyChart && lineChartRef.current) { - toggleGraphsVisibilityInChart({ - graphsVisibilityStates, - lineChartRef, - }); - } - }, [graphsVisibilityStates, canModifyChart]); - const [selectedTime, setSelectedTime] = useState({ name: getSelectedTime()?.name || '', enum: widget?.timePreferance || 'GLOBAL_TIME', }); - const queryKey = useMemo( - () => - `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`, - [selectedTime, globalSelectedTime, widget], - ); - const updatedQuery = useStepInterval(widget?.query); const response = useGetQueryRange( @@ -81,14 +64,19 @@ function FullView({ graphType: widget.panelTypes, query: updatedQuery, globalSelectedInterval: globalSelectedTime, - variables: getDashboardVariables(), + variables: getDashboardVariables(selectedDashboard?.data.variables), }, { - queryKey, + queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`, enabled: !isDependedDataLoaded, }, ); + const canModifyChart = useChartMutable({ + panelType: widget.panelTypes, + panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE, + }); + const chartDataSet = useMemo( () => getChartData({ @@ -101,9 +89,14 @@ function FullView({ [response], ); - const isGraphLegendToggleAvailable = getIsGraphLegendToggleAvailable( - widget.panelTypes, - ); + useEffect(() => { + if (!response.isFetching && lineChartRef.current) { + graphsVisibilityStates?.forEach((e, i) => { + lineChartRef?.current?.toggleGraph(i, e); + parentChartRef?.current?.toggleGraph(i, e); + }); + } + }, [graphsVisibilityStates, parentChartRef, response.isFetching]); if (response.isFetching) { return ; @@ -128,10 +121,10 @@ function FullView({ )} - + )} diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts similarity index 72% rename from frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts index 9e5bd09541..b73a2e9112 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts @@ -31,26 +31,6 @@ export const GraphContainer = styled.div` isGraphLegendToggleAvailable ? '50%' : '100%'}; `; -export const FilterTableAndSaveContainer = styled.div` - margin-top: 1.875rem; - display: flex; - align-items: flex-end; -`; - -export const FilterTableContainer = styled.div` - flex-basis: 80%; -`; - -export const SaveContainer = styled.div` - flex-basis: 20%; - display: flex; - justify-content: flex-end; -`; - -export const SaveCancelButtonContainer = styled.span` - margin: 0 0.313rem; -`; - export const LabelContainer = styled.button` max-width: 18.75rem; cursor: pointer; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/types.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts similarity index 76% rename from frontend/src/container/GridGraphLayout/Graph/FullView/types.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/types.ts index 7d329e1399..ae686496e5 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts @@ -1,7 +1,8 @@ import { CheckboxChangeEvent } from 'antd/es/checkbox'; import { ChartData, ChartDataset } from 'chart.js'; -import { GraphOnClickHandler } from 'components/Graph/types'; +import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import { MutableRefObject } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; export interface DataSetProps { @@ -40,20 +41,6 @@ export interface LabelProps { label: string; } -export interface GraphManagerProps { - data: ChartData; - name: string; - yAxisUnit?: string; - onToggleModelHandler?: () => void; -} - -export interface CheckBoxProps { - data: ChartData; - index: number; - graphVisibilityState: boolean[]; - checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void; -} - export interface FullViewProps { widget: Widgets; fullViewOptions?: boolean; @@ -64,6 +51,26 @@ export interface FullViewProps { isDependedDataLoaded?: boolean; graphsVisibilityStates?: boolean[]; onToggleModelHandler?: GraphManagerProps['onToggleModelHandler']; + setGraphsVisibilityStates: (graphsVisibilityStates: boolean[]) => void; + parentChartRef: GraphManagerProps['lineChartRef']; +} + +export interface GraphManagerProps { + data: ChartData; + name: string; + yAxisUnit?: string; + onToggleModelHandler?: () => void; + setGraphsVisibilityStates: FullViewProps['setGraphsVisibilityStates']; + graphsVisibilityStates: FullViewProps['graphsVisibilityStates']; + lineChartRef?: MutableRefObject; + parentChartRef?: MutableRefObject; +} + +export interface CheckBoxProps { + data: ChartData; + index: number; + graphVisibilityState: boolean[]; + checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void; } export interface SaveLegendEntriesToLocalStoreProps { diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts similarity index 94% rename from frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts index 256bc39050..b1ffb3a032 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts @@ -1,6 +1,5 @@ import { ChartData, ChartDataset } from 'chart.js'; import { LOCALSTORAGE } from 'constants/localStorage'; -import { PANEL_TYPES } from 'constants/queryBuilder'; import { ExtendedChartDataset, @@ -110,10 +109,6 @@ export const saveLegendEntriesToLocalStorage = ({ } }; -export const getIsGraphLegendToggleAvailable = ( - panelType: PANEL_TYPES, -): boolean => panelType === PANEL_TYPES.TIME_SERIES; - export const getGraphManagerTableHeaderTitle = ( title: string, yAxisUnit?: string, diff --git a/frontend/src/container/GridGraphLayout/Graph/Graph.test.tsx b/frontend/src/container/GridCardLayout/GridCard/Graph.test.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/Graph.test.tsx rename to frontend/src/container/GridCardLayout/GridCard/Graph.test.tsx diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx new file mode 100644 index 0000000000..c011ca2471 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -0,0 +1,277 @@ +import { Typography } from 'antd'; +import { ToggleGraphProps } from 'components/Graph/types'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import GridPanelSwitch from 'container/GridPanelSwitch'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; +import { useNotifications } from 'hooks/useNotifications'; +import createQueryParams from 'lib/createQueryParams'; +import history from 'lib/history'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import { Dashboard } from 'types/api/dashboard/getAll'; +import AppReducer from 'types/reducer/app'; +import { v4 } from 'uuid'; + +import WidgetHeader from '../WidgetHeader'; +import FullView from './FullView'; +import { FullViewContainer, Modal } from './styles'; +import { WidgetGraphComponentProps } from './types'; +import { getGraphVisibilityStateOnDataChange } from './utils'; + +function WidgetGraphComponent({ + data, + widget, + queryResponse, + errorMessage, + name, + onDragSelect, + onClickHandler, + threshold, + headerMenuList, + isWarning, +}: WidgetGraphComponentProps): JSX.Element { + const [deleteModal, setDeleteModal] = useState(false); + const [modal, setModal] = useState(false); + const [hovered, setHovered] = useState(false); + const { notifications } = useNotifications(); + const { pathname } = useLocation(); + + const lineChartRef = useRef(); + + const { graphVisibilityStates: localStoredVisibilityStates } = useMemo( + () => + getGraphVisibilityStateOnDataChange({ + data, + isExpandedName: true, + name, + }), + [data, name], + ); + + useEffect(() => { + if (!lineChartRef.current) return; + + localStoredVisibilityStates.forEach((state, index) => { + lineChartRef.current?.toggleGraph(index, state); + }); + }, [localStoredVisibilityStates]); + + const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard(); + + const [graphsVisibilityStates, setGraphsVisibilityStates] = useState< + boolean[] + >(localStoredVisibilityStates); + + const { featureResponse } = useSelector( + (state) => state.app, + ); + const onToggleModal = useCallback( + (func: Dispatch>) => { + func((value) => !value); + }, + [], + ); + + const updateDashboardMutation = useUpdateDashboard(); + + const onDeleteHandler = (): void => { + if (!selectedDashboard) return; + + const updatedWidgets = selectedDashboard?.data?.widgets?.filter( + (e) => e.id !== widget.id, + ); + + const updatedLayout = + selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || []; + + const updatedSelectedDashboard: Dashboard = { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + widgets: updatedWidgets, + layout: updatedLayout, + }, + uuid: selectedDashboard.uuid, + }; + + updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { + onSuccess: (updatedDashboard) => { + if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); + if (setSelectedDashboard && updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + } + featureResponse.refetch(); + }, + onError: () => { + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + }, + }); + }; + + const onCloneHandler = async (): Promise => { + if (!selectedDashboard) return; + + const uuid = v4(); + + const layout = [ + ...(selectedDashboard.data.layout || []), + { + i: uuid, + w: 6, + x: 0, + h: 2, + y: 0, + }, + ]; + + updateDashboardMutation.mutateAsync( + { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + layout, + widgets: [ + ...(selectedDashboard.data.widgets || []), + { + ...{ + ...widget, + id: uuid, + }, + }, + ], + }, + }, + { + onSuccess: () => { + notifications.success({ + message: 'Panel cloned successfully, redirecting to new copy.', + }); + const queryParams = { + graphType: widget?.panelTypes, + widgetId: uuid, + }; + history.push(`${pathname}/new?${createQueryParams(queryParams)}`); + }, + }, + ); + }; + + const handleOnView = (): void => { + onToggleModal(setModal); + }; + + const handleOnDelete = (): void => { + onToggleModal(setDeleteModal); + }; + + const onDeleteModelHandler = (): void => { + onToggleModal(setDeleteModal); + }; + + const onToggleModelHandler = (): void => { + onToggleModal(setModal); + }; + + return ( + { + setHovered(true); + }} + onFocus={(): void => { + setHovered(true); + }} + onMouseOut={(): void => { + setHovered(false); + }} + onBlur={(): void => { + setHovered(false); + }} + > + + Are you sure you want to delete this widget + + + + + + + + +
+ +
+ +
+ ); +} + +WidgetGraphComponent.defaultProps = { + yAxisUnit: undefined, + setLayout: undefined, + onDragSelect: undefined, + onClickHandler: undefined, +}; + +export default WidgetGraphComponent; diff --git a/frontend/src/container/GridGraphLayout/Graph/__mock__/mockChartData.ts b/frontend/src/container/GridCardLayout/GridCard/__mock__/mockChartData.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/__mock__/mockChartData.ts rename to frontend/src/container/GridCardLayout/GridCard/__mock__/mockChartData.ts diff --git a/frontend/src/container/GridGraphLayout/Graph/__mock__/mockLegendEntryData.ts b/frontend/src/container/GridCardLayout/GridCard/__mock__/mockLegendEntryData.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/__mock__/mockLegendEntryData.ts rename to frontend/src/container/GridCardLayout/GridCard/__mock__/mockLegendEntryData.ts diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx new file mode 100644 index 0000000000..598f4dd708 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -0,0 +1,134 @@ +import { Skeleton } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; +import getChartData from 'lib/getChartData'; +import isEmpty from 'lodash-es/isEmpty'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { memo, useMemo, useState } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { useDispatch, useSelector } from 'react-redux'; +import { UpdateTimeInterval } from 'store/actions'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import EmptyWidget from '../EmptyWidget'; +import { MenuItemKeys } from '../WidgetHeader/contants'; +import { GridCardGraphProps } from './types'; +import WidgetGraphComponent from './WidgetGraphComponent'; + +function GridCardGraph({ + widget, + name, + onClickHandler, + headerMenuList = [MenuItemKeys.View], + isQueryEnabled, + threshold, +}: GridCardGraphProps): JSX.Element { + const dispatch = useDispatch(); + const [errorMessage, setErrorMessage] = useState(); + + const onDragSelect = (start: number, end: number): void => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + if (startTimestamp !== endTimestamp) { + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + } + }; + + const { ref: graphRef, inView: isGraphVisible } = useInView({ + threshold: 0, + triggerOnce: true, + initialInView: false, + }); + + const { selectedDashboard } = useDashboard(); + + const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const updatedQuery = useStepInterval(widget?.query); + + const isEmptyWidget = + widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget); + + const queryResponse = useGetQueryRange( + { + selectedTime: widget?.timePreferance, + graphType: widget?.panelTypes, + query: updatedQuery, + globalSelectedInterval, + variables: getDashboardVariables(selectedDashboard?.data.variables), + }, + { + queryKey: [ + maxTime, + minTime, + globalSelectedInterval, + selectedDashboard?.data?.variables, + widget?.query, + widget?.panelTypes, + widget.timePreferance, + ], + keepPreviousData: true, + enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled, + refetchOnMount: false, + onError: (error) => { + setErrorMessage(error.message); + }, + }, + ); + + const chartData = useMemo( + () => + getChartData({ + queryData: [ + { + queryData: queryResponse?.data?.payload?.data?.result || [], + }, + ], + createDataset: undefined, + isWarningLimit: true, + }), + [queryResponse], + ); + + const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET; + + if (queryResponse.isLoading) { + return ; + } + + return ( + + + + {isEmptyLayout && } + + ); +} + +GridCardGraph.defaultProps = { + onDragSelect: undefined, + onClickHandler: undefined, + isQueryEnabled: true, + threshold: undefined, + headerMenuList: [MenuItemKeys.View], +}; + +export default memo(GridCardGraph); diff --git a/frontend/src/container/GridGraphLayout/Graph/styles.ts b/frontend/src/container/GridCardLayout/GridCard/styles.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/styles.ts rename to frontend/src/container/GridCardLayout/GridCard/styles.ts diff --git a/frontend/src/container/GridGraphLayout/Graph/types.ts b/frontend/src/container/GridCardLayout/GridCard/types.ts similarity index 67% rename from frontend/src/container/GridGraphLayout/Graph/types.ts rename to frontend/src/container/GridCardLayout/GridCard/types.ts index 49b637a178..fccf488dc8 100644 --- a/frontend/src/container/GridGraphLayout/Graph/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/types.ts @@ -1,15 +1,11 @@ import { ChartData } from 'chart.js'; import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types'; -import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react'; -import { Layout } from 'react-grid-layout'; +import { MutableRefObject, ReactNode } from 'react'; import { UseQueryResult } from 'react-query'; -import { DeleteWidgetProps } from 'store/actions/dashboard/deleteWidget'; -import AppActions from 'types/actions'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { Widgets } from 'types/api/dashboard/getAll'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; -import { LayoutProps } from '..'; import { MenuItemKeys } from '../WidgetHeader/contants'; import { LegendEntryProps } from './FullView/types'; @@ -18,15 +14,7 @@ export interface GraphVisibilityLegendEntryProps { legendEntry: LegendEntryProps[]; } -export interface DispatchProps { - deleteWidget: ({ - widgetId, - }: DeleteWidgetProps) => (dispatch: Dispatch) => void; -} - -export interface WidgetGraphComponentProps extends DispatchProps { - enableModel: boolean; - enableWidgetHeader: boolean; +export interface WidgetGraphComponentProps { widget: Widgets; queryResponse: UseQueryResult< SuccessResponse | ErrorResponse @@ -34,21 +22,16 @@ export interface WidgetGraphComponentProps extends DispatchProps { errorMessage: string | undefined; data: ChartData; name: string; - yAxisUnit?: string; - layout?: Layout[]; - setLayout?: Dispatch>; onDragSelect?: (start: number, end: number) => void; onClickHandler?: GraphOnClickHandler; threshold?: ReactNode; headerMenuList: MenuItemKeys[]; + isWarning: boolean; } export interface GridCardGraphProps { widget: Widgets; name: string; - yAxisUnit: string | undefined; - layout?: Layout[]; - setLayout?: Dispatch>; onDragSelect?: (start: number, end: number) => void; onClickHandler?: GraphOnClickHandler; threshold?: ReactNode; diff --git a/frontend/src/container/GridGraphLayout/Graph/utils.ts b/frontend/src/container/GridCardLayout/GridCard/utils.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/utils.ts rename to frontend/src/container/GridCardLayout/GridCard/utils.ts diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx new file mode 100644 index 0000000000..b99f2396c9 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -0,0 +1,141 @@ +import { PlusOutlined, SaveFilled } from '@ant-design/icons'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; +import useComponentPermission from 'hooks/useComponentPermission'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useNotifications } from 'hooks/useNotifications'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; +import AppReducer from 'types/reducer/app'; + +import { headerMenuList } from './config'; +import GridCard from './GridCard'; +import { + Button, + ButtonContainer, + Card, + CardContainer, + ReactGridLayout, +} from './styles'; +import { GraphLayoutProps } from './types'; + +function GraphLayout({ + onAddPanelHandler, + widgets, +}: GraphLayoutProps): JSX.Element { + const { + selectedDashboard, + layouts, + setLayouts, + setSelectedDashboard, + } = useDashboard(); + const { t } = useTranslation(['dashboard']); + + const { featureResponse, role } = useSelector( + (state) => state.app, + ); + + const isDarkMode = useIsDarkMode(); + + const updateDashboardMutation = useUpdateDashboard(); + + const { notifications } = useNotifications(); + + const [saveLayoutPermission, addPanelPermission] = useComponentPermission( + ['save_layout', 'add_panel'], + role, + ); + + const onSaveHandler = (): void => { + if (!selectedDashboard) return; + + const updatedDashboard: Dashboard = { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + layout: layouts.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET), + }, + uuid: selectedDashboard.uuid, + }; + + updateDashboardMutation.mutate(updatedDashboard, { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + if (updatedDashboard.payload.data.layout) + setLayouts(updatedDashboard.payload.data.layout); + setSelectedDashboard(updatedDashboard.payload); + } + notifications.success({ + message: t('dashboard:layout_saved_successfully'), + }); + + featureResponse.refetch(); + }, + onError: () => { + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + }, + }); + }; + + return ( + <> + + {saveLayoutPermission && ( + + )} + + {addPanelPermission && ( + + )} + + + + {layouts.map((layout) => { + const { i: id } = layout; + const currentWidget = (widgets || [])?.find((e) => e.id === id); + + return ( + + + + + + ); + })} + + + ); +} + +export default GraphLayout; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/DisplayThreshold.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/DisplayThreshold.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/DisplayThreshold.tsx rename to frontend/src/container/GridCardLayout/WidgetHeader/DisplayThreshold.tsx diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/config.ts b/frontend/src/container/GridCardLayout/WidgetHeader/config.ts similarity index 57% rename from frontend/src/container/GridGraphLayout/WidgetHeader/config.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/config.ts index 1e15697049..2a8f2d76ee 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/config.ts +++ b/frontend/src/container/GridCardLayout/WidgetHeader/config.ts @@ -1,9 +1,14 @@ import { themeColors } from 'constants/theme'; +import { limit } from 'lib/getChartData'; import { CSSProperties } from 'react'; const positionCss: CSSProperties['position'] = 'absolute'; -export const spinnerStyles = { position: positionCss, right: '0.5rem' }; +export const spinnerStyles = { + position: positionCss, + top: '0', + right: '0', +}; export const tooltipStyles = { fontSize: '1rem', top: '0.313rem', @@ -21,3 +26,5 @@ export const overlayStyles: CSSProperties = { justifyContent: 'center', position: 'absolute', }; + +export const WARNING_MESSAGE = `Too many timeseries in the result. UI has restricted to showing the top ${limit}. Please check the query if this is needed and contact support@signoz.io if you need to show >${limit} timeseries in the panel`; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/contants.ts b/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/contants.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/contants.ts diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx similarity index 92% rename from frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx rename to frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index 20ad222e55..a916534082 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -5,10 +5,12 @@ import { EditFilled, ExclamationCircleOutlined, FullscreenOutlined, + WarningOutlined, } from '@ant-design/icons'; import { Dropdown, MenuProps, Tooltip, Typography } from 'antd'; import Spinner from 'components/Spinner'; import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; @@ -27,6 +29,7 @@ import { overlayStyles, spinnerStyles, tooltipStyles, + WARNING_MESSAGE, } from './config'; import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants'; import { @@ -52,6 +55,7 @@ interface IWidgetHeaderProps { errorMessage: string | undefined; threshold?: ReactNode; headerMenuList?: MenuItemKeys[]; + isWarning: boolean; } function WidgetHeader({ @@ -65,7 +69,8 @@ function WidgetHeader({ errorMessage, threshold, headerMenuList, -}: IWidgetHeaderProps): JSX.Element { + isWarning, +}: IWidgetHeaderProps): JSX.Element | null { const [localHover, setLocalHover] = useState(false); const [isOpen, setIsOpen] = useState(false); @@ -126,7 +131,7 @@ function WidgetHeader({ icon: , label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.View], isVisible: headerMenuList?.includes(MenuItemKeys.View) || false, - disabled: queryResponse.isLoading, + disabled: queryResponse.isFetching, }, { key: MenuItemKeys.Edit, @@ -158,7 +163,7 @@ function WidgetHeader({ disabled: false, }, ], - [queryResponse.isLoading, headerMenuList, editWidget, deleteWidget], + [headerMenuList, queryResponse.isFetching, editWidget, deleteWidget], ); const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]); @@ -175,6 +180,10 @@ function WidgetHeader({ [updatedMenuList, onMenuItemSelectHandler], ); + if (widget.id === PANEL_TYPES.EMPTY_WIDGET) { + return null; + } + return ( + {threshold} {queryResponse.isFetching && !queryResponse.isError && ( @@ -211,6 +221,12 @@ function WidgetHeader({ )} + + {isWarning && ( + + + + )} ); } diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts b/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/styles.ts diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/types.ts b/frontend/src/container/GridCardLayout/WidgetHeader/types.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/types.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/types.ts diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/utils.ts b/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/utils.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/utils.ts diff --git a/frontend/src/container/GridCardLayout/config.ts b/frontend/src/container/GridCardLayout/config.ts new file mode 100644 index 0000000000..3fa9e8e569 --- /dev/null +++ b/frontend/src/container/GridCardLayout/config.ts @@ -0,0 +1,17 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants'; + +export const headerMenuList = [ + MenuItemKeys.View, + MenuItemKeys.Clone, + MenuItemKeys.Delete, + MenuItemKeys.Edit, +]; + +export const EMPTY_WIDGET_LAYOUT = { + i: PANEL_TYPES.EMPTY_WIDGET, + w: 6, + x: 0, + h: 2, + y: 0, +}; diff --git a/frontend/src/container/GridCardLayout/index.tsx b/frontend/src/container/GridCardLayout/index.tsx new file mode 100644 index 0000000000..e715d7d539 --- /dev/null +++ b/frontend/src/container/GridCardLayout/index.tsx @@ -0,0 +1,35 @@ +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useCallback } from 'react'; +import { Layout } from 'react-grid-layout'; + +import { EMPTY_WIDGET_LAYOUT } from './config'; +import GraphLayoutContainer from './GridCardLayout'; + +function GridGraph(): JSX.Element { + const { + selectedDashboard, + setLayouts, + handleToggleDashboardSlider, + } = useDashboard(); + + const { data } = selectedDashboard || {}; + const { widgets } = data || {}; + + const onEmptyWidgetHandler = useCallback(() => { + handleToggleDashboardSlider(true); + + setLayouts((preLayout: Layout[]) => [ + EMPTY_WIDGET_LAYOUT, + ...(preLayout || []), + ]); + }, [handleToggleDashboardSlider, setLayouts]); + + return ( + + ); +} + +export default GridGraph; diff --git a/frontend/src/container/GridGraphLayout/styles.ts b/frontend/src/container/GridCardLayout/styles.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/styles.ts rename to frontend/src/container/GridCardLayout/styles.ts diff --git a/frontend/src/container/GridCardLayout/types.ts b/frontend/src/container/GridCardLayout/types.ts new file mode 100644 index 0000000000..0d2b678af6 --- /dev/null +++ b/frontend/src/container/GridCardLayout/types.ts @@ -0,0 +1,6 @@ +import { Widgets } from 'types/api/dashboard/getAll'; + +export interface GraphLayoutProps { + onAddPanelHandler: VoidFunction; + widgets?: Widgets[]; +} diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/GraphManager.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/GraphManager.tsx deleted file mode 100644 index 61abbf2aa6..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/GraphManager.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { Button, Input } from 'antd'; -import { CheckboxChangeEvent } from 'antd/es/checkbox'; -import { ResizeTable } from 'components/ResizeTable'; -import { Events } from 'constants/events'; -import { useNotifications } from 'hooks/useNotifications'; -import isEqual from 'lodash-es/isEqual'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { eventEmitter } from 'utils/getEventEmitter'; - -import { getGraphVisibilityStateOnDataChange } from '../utils'; -import { - FilterTableAndSaveContainer, - FilterTableContainer, - SaveCancelButtonContainer, - SaveContainer, -} from './styles'; -import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns'; -import { ExtendedChartDataset, GraphManagerProps } from './types'; -import { - getDefaultTableDataSet, - saveLegendEntriesToLocalStorage, -} from './utils'; - -function GraphManager({ - data, - name, - yAxisUnit, - onToggleModelHandler, -}: GraphManagerProps): JSX.Element { - const { - graphVisibilityStates: localstoredVisibilityStates, - legendEntry, - } = useMemo( - () => - getGraphVisibilityStateOnDataChange({ - data, - isExpandedName: false, - name, - }), - [data, name], - ); - - const [graphVisibilityState, setGraphVisibilityState] = useState( - localstoredVisibilityStates, - ); - - const [tableDataSet, setTableDataSet] = useState( - getDefaultTableDataSet(data), - ); - - const { notifications } = useNotifications(); - - // useEffect for updating graph visibility state on data change - useEffect(() => { - const newGraphVisibilityStates = Array(data.datasets.length).fill( - true, - ); - data.datasets.forEach((dataset, i) => { - const index = legendEntry.findIndex( - (entry) => entry.label === dataset.label, - ); - if (index !== -1) { - newGraphVisibilityStates[i] = legendEntry[index].show; - } - }); - eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, { - name, - graphVisibilityStates: newGraphVisibilityStates, - }); - setGraphVisibilityState(newGraphVisibilityStates); - }, [data, name, legendEntry]); - - // useEffect for listening to events event graph legend is clicked - useEffect(() => { - const eventListener = eventEmitter.on( - Events.UPDATE_GRAPH_MANAGER_TABLE, - (data) => { - if (data.name === name) { - const newGraphVisibilityStates = graphVisibilityState; - newGraphVisibilityStates[data.index] = !newGraphVisibilityStates[ - data.index - ]; - eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, { - name, - graphVisibilityStates: newGraphVisibilityStates, - }); - setGraphVisibilityState([...newGraphVisibilityStates]); - } - }, - ); - return (): void => { - eventListener.off(Events.UPDATE_GRAPH_MANAGER_TABLE); - }; - }, [graphVisibilityState, name]); - - const checkBoxOnChangeHandler = useCallback( - (e: CheckboxChangeEvent, index: number): void => { - graphVisibilityState[index] = e.target.checked; - setGraphVisibilityState([...graphVisibilityState]); - eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, { - name, - graphVisibilityStates: [...graphVisibilityState], - }); - }, - [graphVisibilityState, name], - ); - - const labelClickedHandler = useCallback( - (labelIndex: number): void => { - const newGraphVisibilityStates = Array(data.datasets.length).fill( - false, - ); - newGraphVisibilityStates[labelIndex] = true; - setGraphVisibilityState([...newGraphVisibilityStates]); - eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, { - name, - graphVisibilityStates: newGraphVisibilityStates, - }); - }, - [data.datasets.length, name], - ); - - const columns = useMemo( - () => - getGraphManagerTableColumns({ - data, - checkBoxOnChangeHandler, - graphVisibilityState, - labelClickedHandler, - yAxisUnit, - }), - [ - checkBoxOnChangeHandler, - data, - graphVisibilityState, - labelClickedHandler, - yAxisUnit, - ], - ); - - const filterHandler = useCallback( - (event: React.ChangeEvent): void => { - const value = event.target.value.toString().toLowerCase(); - const updatedDataSet = tableDataSet.map((item) => { - if (item.label?.toLocaleLowerCase().includes(value)) { - return { ...item, show: true }; - } - return { ...item, show: false }; - }); - setTableDataSet(updatedDataSet); - }, - [tableDataSet], - ); - - const saveHandler = useCallback((): void => { - saveLegendEntriesToLocalStorage({ - data, - graphVisibilityState, - name, - }); - notifications.success({ - message: 'The updated graphs & legends are saved', - }); - if (onToggleModelHandler) { - onToggleModelHandler(); - } - }, [data, graphVisibilityState, name, notifications, onToggleModelHandler]); - - const dataSource = tableDataSet.filter((item) => item.show); - - return ( - - - - - - - - - - - - - - - ); -} - -GraphManager.defaultProps = { - graphVisibilityStateHandler: undefined, -}; - -export default memo( - GraphManager, - (prevProps, nextProps) => - isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name, -); diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetCheckBox.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetCheckBox.tsx deleted file mode 100644 index 55485be5ad..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetCheckBox.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { CheckboxChangeEvent } from 'antd/es/checkbox'; -import { ColumnType } from 'antd/es/table'; -import { ChartData } from 'chart.js'; - -import { DataSetProps } from '../types'; -import CustomCheckBox from './CustomCheckBox'; - -export const getCheckBox = ({ - data, - checkBoxOnChangeHandler, - graphVisibilityState, -}: GetCheckBoxProps): ColumnType => ({ - render: (index: number): JSX.Element => ( - - ), -}); - -interface GetCheckBoxProps { - data: ChartData; - checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void; - graphVisibilityState: boolean[]; -} diff --git a/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx b/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx deleted file mode 100644 index f40d1e4190..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import { Typography } from 'antd'; -import { ToggleGraphProps } from 'components/Graph/types'; -import { Events } from 'constants/events'; -import GridPanelSwitch from 'container/GridPanelSwitch'; -import { useChartMutable } from 'hooks/useChartMutable'; -import { useNotifications } from 'hooks/useNotifications'; -import createQueryParams from 'lib/createQueryParams'; -import history from 'lib/history'; -import { isEmpty, isEqual } from 'lodash-es'; -import { - Dispatch, - memo, - SetStateAction, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { useTranslation } from 'react-i18next'; -import { connect, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { DeleteWidget } from 'store/actions/dashboard/deleteWidget'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; -import { eventEmitter } from 'utils/getEventEmitter'; -import { v4 } from 'uuid'; - -import { UpdateDashboard } from '../utils'; -import WidgetHeader from '../WidgetHeader'; -import FullView from './FullView'; -import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './FullView/contants'; -import { FullViewContainer, Modal } from './styles'; -import { DispatchProps, WidgetGraphComponentProps } from './types'; -import { - getGraphVisibilityStateOnDataChange, - toggleGraphsVisibilityInChart, -} from './utils'; - -function WidgetGraphComponent({ - enableModel, - enableWidgetHeader, - data, - widget, - queryResponse, - errorMessage, - name, - yAxisUnit, - layout = [], - deleteWidget, - setLayout, - onDragSelect, - onClickHandler, - threshold, - headerMenuList, -}: WidgetGraphComponentProps): JSX.Element { - const [deleteModal, setDeleteModal] = useState(false); - const [modal, setModal] = useState(false); - const [hovered, setHovered] = useState(false); - const { notifications } = useNotifications(); - const { t } = useTranslation(['common']); - const { pathname } = useLocation(); - - const { graphVisibilityStates: localstoredVisibilityStates } = useMemo( - () => - getGraphVisibilityStateOnDataChange({ - data, - isExpandedName: true, - name, - }), - [data, name], - ); - - const [graphsVisibilityStates, setGraphsVisilityStates] = useState( - localstoredVisibilityStates, - ); - - const { dashboards } = useSelector( - (state) => state.dashboards, - ); - const [selectedDashboard] = dashboards; - - const canModifyChart = useChartMutable({ - panelType: widget.panelTypes, - panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE, - }); - - const lineChartRef = useRef(); - - // Updating the visibility state of the graph on data change according to global time range - useEffect(() => { - if (canModifyChart) { - const newGraphVisibilityState = getGraphVisibilityStateOnDataChange({ - data, - isExpandedName: true, - name, - }); - setGraphsVisilityStates(newGraphVisibilityState.graphVisibilityStates); - } - }, [canModifyChart, data, name]); - - useEffect(() => { - const eventListener = eventEmitter.on( - Events.UPDATE_GRAPH_VISIBILITY_STATE, - (data) => { - if (data.name === `${name}expanded` && canModifyChart) { - setGraphsVisilityStates([...data.graphVisibilityStates]); - } - }, - ); - return (): void => { - eventListener.off(Events.UPDATE_GRAPH_VISIBILITY_STATE); - }; - }, [canModifyChart, name]); - - useEffect(() => { - if (canModifyChart && lineChartRef.current) { - toggleGraphsVisibilityInChart({ - graphsVisibilityStates, - lineChartRef, - }); - } - }, [graphsVisibilityStates, canModifyChart]); - - const { featureResponse } = useSelector( - (state) => state.app, - ); - const onToggleModal = useCallback( - (func: Dispatch>) => { - func((value) => !value); - }, - [], - ); - - const onDeleteHandler = useCallback(() => { - const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget); - const widgetId = isEmptyWidget ? layout[0].i : widget?.id; - - featureResponse - .refetch() - .then(() => { - deleteWidget({ widgetId, setLayout }); - onToggleModal(setDeleteModal); - }) - .catch(() => { - notifications.error({ - message: t('common:something_went_wrong'), - }); - }); - }, [ - widget, - layout, - featureResponse, - deleteWidget, - setLayout, - onToggleModal, - notifications, - t, - ]); - - const onCloneHandler = async (): Promise => { - const uuid = v4(); - - const layout = [ - { - i: uuid, - w: 6, - x: 0, - h: 2, - y: 0, - }, - ...(selectedDashboard.data.layout || []), - ]; - - if (widget) { - await UpdateDashboard( - { - data: selectedDashboard.data, - generateWidgetId: uuid, - graphType: widget?.panelTypes, - selectedDashboard, - layout, - widgetData: widget, - isRedirected: false, - }, - notifications, - ).then(() => { - notifications.success({ - message: 'Panel cloned successfully, redirecting to new copy.', - }); - - const queryParams = { - graphType: widget?.panelTypes, - widgetId: uuid, - }; - history.push(`${pathname}/new?${createQueryParams(queryParams)}`); - }); - } - }; - - const handleOnView = (): void => { - onToggleModal(setModal); - }; - - const handleOnDelete = (): void => { - onToggleModal(setDeleteModal); - }; - - const onDeleteModelHandler = (): void => { - onToggleModal(setDeleteModal); - }; - - const onToggleModelHandler = (): void => { - onToggleModal(setModal); - }; - - const getModals = (): JSX.Element => ( - <> - - Are you sure you want to delete this widget - - - - - - - - - ); - - return ( - { - setHovered(true); - }} - onFocus={(): void => { - setHovered(true); - }} - onMouseOut={(): void => { - setHovered(false); - }} - onBlur={(): void => { - setHovered(false); - }} - > - {enableModel && getModals()} - {!isEmpty(widget) && data && ( - <> - {enableWidgetHeader && ( -
- -
- )} - - - )} -
- ); -} - -WidgetGraphComponent.defaultProps = { - yAxisUnit: undefined, - layout: undefined, - setLayout: undefined, - onDragSelect: undefined, - onClickHandler: undefined, -}; - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - deleteWidget: bindActionCreators(DeleteWidget, dispatch), -}); - -export default connect( - null, - mapDispatchToProps, -)( - memo( - WidgetGraphComponent, - (prevProps, nextProps) => - isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name, - ), -); diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx deleted file mode 100644 index 94b9d10252..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { ChartData } from 'chart.js'; -import Spinner from 'components/Spinner'; -import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; -import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; -import usePreviousValue from 'hooks/usePreviousValue'; -import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; -import getChartData from 'lib/getChartData'; -import isEmpty from 'lodash-es/isEmpty'; -import { memo, useMemo, useState } from 'react'; -import { useInView } from 'react-intersection-observer'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import DashboardReducer from 'types/reducer/dashboards'; -import { GlobalReducer } from 'types/reducer/globalTime'; -import { getSelectedDashboardVariable } from 'utils/dashboard/selectedDashboard'; - -import EmptyWidget from '../EmptyWidget'; -import { MenuItemKeys } from '../WidgetHeader/contants'; -import { GridCardGraphProps } from './types'; -import WidgetGraphComponent from './WidgetGraphComponent'; - -function GridCardGraph({ - widget, - name, - yAxisUnit, - layout = [], - setLayout, - onDragSelect, - onClickHandler, - headerMenuList = [MenuItemKeys.View], - isQueryEnabled, - threshold, -}: GridCardGraphProps): JSX.Element { - const { isAddWidget } = useSelector( - (state) => state.dashboards, - ); - - const { ref: graphRef, inView: isGraphVisible } = useInView({ - threshold: 0, - triggerOnce: true, - initialInView: false, - }); - - const [errorMessage, setErrorMessage] = useState(''); - - const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< - AppState, - GlobalReducer - >((state) => state.globalTime); - const { dashboards } = useSelector( - (state) => state.dashboards, - ); - - const variables = getSelectedDashboardVariable(dashboards); - - const updatedQuery = useStepInterval(widget?.query); - - const isEmptyWidget = useMemo( - () => widget?.id === 'empty' || isEmpty(widget), - [widget], - ); - - const queryResponse = useGetQueryRange( - { - selectedTime: widget?.timePreferance, - graphType: widget?.panelTypes, - query: updatedQuery, - globalSelectedInterval, - variables: getDashboardVariables(), - }, - { - queryKey: [ - `GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget?.id}`, - maxTime, - minTime, - globalSelectedInterval, - variables, - widget?.query, - widget?.panelTypes, - ], - keepPreviousData: true, - enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled && !isAddWidget, - refetchOnMount: false, - onError: (error) => { - setErrorMessage(error.message); - }, - }, - ); - - const chartData = useMemo( - () => - getChartData({ - queryData: [ - { - queryData: queryResponse?.data?.payload?.data?.result || [], - }, - ], - }), - [queryResponse], - ); - - const prevChartDataSetRef = usePreviousValue(chartData); - - const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget); - - if (queryResponse.isRefetching || queryResponse.isLoading) { - return ; - } - - if ((queryResponse.isError && !isEmptyLayout) || !isQueryEnabled) { - return ( - - {!isEmpty(widget) && prevChartDataSetRef && ( - - )} - - ); - } - - if (!isEmpty(widget) && prevChartDataSetRef?.labels) { - return ( - - - - ); - } - - return ( - - {!isEmpty(widget) && !!queryResponse.data?.payload && ( - - )} - - {isEmptyLayout && } - - ); -} - -GridCardGraph.defaultProps = { - onDragSelect: undefined, - onClickHandler: undefined, - isQueryEnabled: true, - threshold: undefined, - headerMenuList: [MenuItemKeys.View], -}; - -export default memo(GridCardGraph); diff --git a/frontend/src/container/GridGraphLayout/GraphLayout.tsx b/frontend/src/container/GridGraphLayout/GraphLayout.tsx deleted file mode 100644 index 6fc6aca63f..0000000000 --- a/frontend/src/container/GridGraphLayout/GraphLayout.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { PlusOutlined, SaveFilled } from '@ant-design/icons'; -import { PANEL_TYPES } from 'constants/queryBuilder'; -import useComponentPermission from 'hooks/useComponentPermission'; -import { useIsDarkMode } from 'hooks/useDarkMode'; -import { Dispatch, SetStateAction } from 'react'; -import { Layout } from 'react-grid-layout'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { Widgets } from 'types/api/dashboard/getAll'; -import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; - -import { LayoutProps, State } from '.'; -import { - Button, - ButtonContainer, - Card, - CardContainer, - ReactGridLayout, -} from './styles'; - -function GraphLayout({ - layouts, - saveLayoutState, - onLayoutSaveHandler, - addPanelLoading, - onAddPanelHandler, - onLayoutChangeHandler, - widgets, - setLayout, -}: GraphLayoutProps): JSX.Element { - const { isAddWidget } = useSelector( - (state) => state.dashboards, - ); - const { role } = useSelector((state) => state.app); - const isDarkMode = useIsDarkMode(); - - const [saveLayoutPermission, addPanelPermission] = useComponentPermission( - ['save_layout', 'add_panel'], - role, - ); - - return ( - <> - - {saveLayoutPermission && ( - - )} - - {addPanelPermission && ( - - )} - - - - {layouts.map(({ Component, ...rest }) => { - const currentWidget = (widgets || [])?.find((e) => e.id === rest.i); - - return ( - - - - - - ); - })} - - - ); -} - -interface GraphLayoutProps { - layouts: LayoutProps[]; - saveLayoutState: State; - onLayoutSaveHandler: (layout: Layout[]) => Promise; - addPanelLoading: boolean; - onAddPanelHandler: VoidFunction; - onLayoutChangeHandler: (layout: Layout[]) => Promise; - widgets: Widgets[] | undefined; - setLayout: Dispatch>; -} - -export default GraphLayout; diff --git a/frontend/src/container/GridGraphLayout/config.ts b/frontend/src/container/GridGraphLayout/config.ts deleted file mode 100644 index 0357c7795c..0000000000 --- a/frontend/src/container/GridGraphLayout/config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MenuItemKeys } from 'container/GridGraphLayout/WidgetHeader/contants'; - -export const headerMenuList = [ - MenuItemKeys.View, - MenuItemKeys.Clone, - MenuItemKeys.Delete, - MenuItemKeys.Edit, -]; diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx deleted file mode 100644 index fe65154fd9..0000000000 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/* eslint-disable react/no-unstable-nested-components */ - -import updateDashboardApi from 'api/dashboard/update'; -import { PANEL_TYPES } from 'constants/queryBuilder'; -import useComponentPermission from 'hooks/useComponentPermission'; -import { useNotifications } from 'hooks/useNotifications'; -import { - Dispatch, - SetStateAction, - useCallback, - useEffect, - useState, -} from 'react'; -import { Layout } from 'react-grid-layout'; -import { useTranslation } from 'react-i18next'; -import { connect, useDispatch, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch as ReduxDispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { AppDispatch } from 'store'; -import { UpdateTimeInterval } from 'store/actions'; -import { - ToggleAddWidget, - ToggleAddWidgetProps, -} from 'store/actions/dashboard/toggleAddWidget'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { UPDATE_DASHBOARD } from 'types/actions/dashboard'; -import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; -import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; - -import { headerMenuList } from './config'; -import Graph from './Graph'; -import GraphLayoutContainer from './GraphLayout'; -import { UpdateDashboard } from './utils'; - -export const getPreLayouts = ( - widgets: Widgets[] | undefined, - layout: Layout[], -): LayoutProps[] => - layout.map((e, index) => ({ - ...e, - Component: ({ setLayout }: ComponentProps): JSX.Element => { - const widget = widgets?.find((widget) => widget.id === e.i); - - return ( - - ); - }, - })); - -function GridGraph(props: Props): JSX.Element { - const { toggleAddWidget } = props; - const [addPanelLoading, setAddPanelLoading] = useState(false); - const { t } = useTranslation(['common']); - const { dashboards, isAddWidget } = useSelector( - (state) => state.dashboards, - ); - const { role } = useSelector((state) => state.app); - - const [saveLayoutPermission] = useComponentPermission(['save_layout'], role); - const [saveLayoutState, setSaveLayoutState] = useState({ - loading: false, - error: false, - errorMessage: '', - payload: [], - }); - const [selectedDashboard] = dashboards; - const { data } = selectedDashboard; - const { widgets } = data; - const dispatch: AppDispatch = useDispatch>(); - - const [layouts, setLayout] = useState( - getPreLayouts(widgets, selectedDashboard.data.layout || []), - ); - - const onDragSelect = useCallback( - (start: number, end: number) => { - const startTimestamp = Math.trunc(start); - const endTimestamp = Math.trunc(end); - - if (startTimestamp !== endTimestamp) { - dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); - } - }, - [dispatch], - ); - - const { notifications } = useNotifications(); - - useEffect(() => { - (async (): Promise => { - if (!isAddWidget) { - const isEmptyLayoutPresent = layouts.find((e) => e.i === 'empty'); - if (isEmptyLayoutPresent) { - // non empty layout - const updatedLayout = layouts.filter((e) => e.i !== 'empty'); - // non widget - const updatedWidget = widgets?.filter((e) => e.id !== 'empty'); - setLayout(updatedLayout); - - const updatedDashboard: Dashboard = { - ...selectedDashboard, - data: { - ...selectedDashboard.data, - layout: updatedLayout, - widgets: updatedWidget, - }, - }; - - await updateDashboardApi({ - data: updatedDashboard.data, - uuid: updatedDashboard.uuid, - }); - - dispatch({ - type: UPDATE_DASHBOARD, - payload: updatedDashboard, - }); - } - } - })(); - }, [dispatch, isAddWidget, layouts, selectedDashboard, widgets]); - - const { featureResponse } = useSelector( - (state) => state.app, - ); - - const errorMessage = t('common:something_went_wrong'); - - const onLayoutSaveHandler = useCallback( - async (layout: Layout[]) => { - try { - setSaveLayoutState((state) => ({ - ...state, - error: false, - errorMessage: '', - loading: true, - })); - - featureResponse - .refetch() - .then(async () => { - const updatedDashboard: Dashboard = { - ...selectedDashboard, - data: { - title: data.title, - description: data.description, - name: data.name, - tags: data.tags, - widgets: data.widgets, - variables: data.variables, - layout, - }, - uuid: selectedDashboard.uuid, - }; - // Save layout only when users has the has the permission to do so. - if (saveLayoutPermission) { - const response = await updateDashboardApi(updatedDashboard); - if (response.statusCode === 200) { - setSaveLayoutState((state) => ({ - ...state, - error: false, - errorMessage: '', - loading: false, - })); - dispatch({ - type: UPDATE_DASHBOARD, - payload: updatedDashboard, - }); - } else { - setSaveLayoutState((state) => ({ - ...state, - error: true, - errorMessage: response.error || errorMessage, - loading: false, - })); - } - } - }) - .catch(() => { - setSaveLayoutState((state) => ({ - ...state, - error: true, - errorMessage, - loading: false, - })); - notifications.error({ - message: errorMessage, - }); - }); - } catch (error) { - notifications.error({ - message: errorMessage, - }); - } - }, - [ - data.description, - data.name, - data.tags, - data.title, - data.variables, - data.widgets, - dispatch, - errorMessage, - featureResponse, - notifications, - saveLayoutPermission, - selectedDashboard, - ], - ); - - const setLayoutFunction = useCallback( - (layout: Layout[]) => { - setLayout( - layout.map((e) => { - const currentWidget = - widgets?.find((widget) => widget.id === e.i) || ({} as Widgets); - - return { - ...e, - Component: (): JSX.Element => ( - - ), - }; - }), - ); - }, - [widgets, onDragSelect], - ); - - const onEmptyWidgetHandler = useCallback(async () => { - try { - const id = 'empty'; - - const layout = [ - { - i: id, - w: 6, - x: 0, - h: 2, - y: 0, - }, - ...(data.layout || []), - ]; - - await UpdateDashboard( - { - data, - generateWidgetId: id, - graphType: PANEL_TYPES.EMPTY_WIDGET, - selectedDashboard, - layout, - isRedirected: false, - }, - notifications, - ); - - setLayoutFunction(layout); - } catch (error) { - notifications.error({ - message: error instanceof Error ? error.toString() : errorMessage, - }); - } - }, [data, selectedDashboard, setLayoutFunction, notifications, errorMessage]); - - const onLayoutChangeHandler = async (layout: Layout[]): Promise => { - setLayoutFunction(layout); - - // await onLayoutSaveHandler(layout); - }; - - const onAddPanelHandler = useCallback(() => { - try { - setAddPanelLoading(true); - featureResponse - .refetch() - .then(() => { - const isEmptyLayoutPresent = - layouts.find((e) => e.i === 'empty') !== undefined; - - if (!isEmptyLayoutPresent) { - onEmptyWidgetHandler() - .then(() => { - setAddPanelLoading(false); - toggleAddWidget(true); - }) - .catch(() => { - notifications.error({ - message: errorMessage, - }); - }); - } else { - toggleAddWidget(true); - setAddPanelLoading(false); - } - }) - .catch(() => - notifications.error({ - message: errorMessage, - }), - ); - } catch (error) { - notifications.error({ - message: errorMessage, - }); - } - }, [ - featureResponse, - layouts, - onEmptyWidgetHandler, - toggleAddWidget, - notifications, - errorMessage, - ]); - - useEffect( - () => (): void => { - toggleAddWidget(false); - }, - [toggleAddWidget], - ); - - return ( - - ); -} - -interface ComponentProps { - setLayout: Dispatch>; -} - -export interface LayoutProps extends Layout { - Component: (props: ComponentProps) => JSX.Element; -} - -export interface State { - loading: boolean; - error: boolean; - payload: Layout[]; - errorMessage: string; -} - -interface DispatchProps { - toggleAddWidget: ( - props: ToggleAddWidgetProps, - ) => (dispatch: ReduxDispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch), -}); - -type Props = DispatchProps; - -export default connect(null, mapDispatchToProps)(GridGraph); diff --git a/frontend/src/container/GridGraphLayout/utils.ts b/frontend/src/container/GridGraphLayout/utils.ts deleted file mode 100644 index a18fe52886..0000000000 --- a/frontend/src/container/GridGraphLayout/utils.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NotificationInstance } from 'antd/es/notification/interface'; -import updateDashboardApi from 'api/dashboard/update'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; -import { Layout } from 'react-grid-layout'; -import store from 'store'; -import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; - -export const UpdateDashboard = async ( - { - data, - graphType, - generateWidgetId, - layout, - selectedDashboard, - isRedirected, - widgetData, - }: UpdateDashboardProps, - notify: NotificationInstance, -): Promise => { - const copyTitle = `${widgetData?.title} - Copy`; - const updatedSelectedDashboard: Dashboard = { - ...selectedDashboard, - data: { - title: data.title, - description: data.description, - name: data.name, - tags: data.tags, - variables: data.variables, - widgets: [ - ...(data.widgets || []), - { - description: widgetData?.description || '', - id: generateWidgetId, - isStacked: false, - nullZeroValues: widgetData?.nullZeroValues || '', - opacity: '', - panelTypes: graphType, - query: widgetData?.query || initialQueriesMap.metrics, - timePreferance: widgetData?.timePreferance || 'GLOBAL_TIME', - title: widgetData ? copyTitle : '', - yAxisUnit: widgetData?.yAxisUnit, - }, - ], - layout, - }, - uuid: selectedDashboard.uuid, - }; - - const response = await updateDashboardApi(updatedSelectedDashboard); - - if (response.payload) { - store.dispatch({ - type: 'UPDATE_DASHBOARD', - payload: response.payload, - }); - } - - if (isRedirected) { - if (response.statusCode === 200) { - return response.payload; - } - notify.error({ - message: response.error || 'Something went wrong', - }); - return undefined; - } - return undefined; -}; - -interface UpdateDashboardProps { - data: Dashboard['data']; - graphType: PANEL_TYPES; - generateWidgetId: string; - layout: Layout[]; - selectedDashboard: Dashboard; - isRedirected: boolean; - widgetData?: Widgets; -} diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss new file mode 100644 index 0000000000..3d5f41ab33 --- /dev/null +++ b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss @@ -0,0 +1,3 @@ +.ingestion-settings-container { + color: white; +} diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.tsx b/frontend/src/container/IngestionSettings/IngestionSettings.tsx new file mode 100644 index 0000000000..0971ecc960 --- /dev/null +++ b/frontend/src/container/IngestionSettings/IngestionSettings.tsx @@ -0,0 +1,82 @@ +import './IngestionSettings.styles.scss'; + +import { Table, Typography } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import getIngestionData from 'api/settings/getIngestionData'; +import { useQuery } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { IngestionDataType } from 'types/api/settings/ingestion'; +import AppReducer from 'types/reducer/app'; + +export default function IngestionSettings(): JSX.Element { + const { user } = useSelector((state) => state.app); + + const { data: ingestionData } = useQuery({ + queryFn: getIngestionData, + queryKey: ['getIngestionData', user?.userId], + }); + + const columns: ColumnsType = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + render: (text): JSX.Element => {text} , + }, + { + title: 'Value', + dataIndex: 'value', + key: 'value', + render: (text): JSX.Element => ( + {text} + ), + }, + ]; + + const injectionDataPayload = + ingestionData && + ingestionData.payload && + Array.isArray(ingestionData.payload) && + ingestionData?.payload[0]; + + const data: IngestionDataType[] = [ + { + key: '1', + name: 'Ingestion URL', + value: injectionDataPayload?.ingestionURL, + }, + { + key: '2', + name: 'Ingestion Key', + value: injectionDataPayload?.ingestionKey, + }, + { + key: '3', + name: 'Ingestion Region', + value: injectionDataPayload?.dataRegion, + }, + ]; + + return ( +
+ + You can use the following ingestion credentials to start sending your + telemetry data to SigNoz + + + + + ); +} diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index 3a7f406b4e..aa658d56cc 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -9,11 +9,7 @@ import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; import { generatePath } from 'react-router-dom'; -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { FLUSH_DASHBOARD } from 'types/actions/dashboard'; import { DashboardData } from 'types/api/dashboard/getAll'; import { EditorContainer, FooterContainer } from './styles'; @@ -31,8 +27,6 @@ function ImportJSON({ ); const [isFeatureAlert, setIsFeatureAlert] = useState(false); - const dispatch = useDispatch>(); - const [dashboardCreating, setDashboardCreating] = useState(false); const [editorValue, setEditorValue] = useState(''); @@ -77,16 +71,11 @@ function ImportJSON({ }); if (response.statusCode === 200) { - dispatch({ - type: FLUSH_DASHBOARD, - }); - setTimeout(() => { - history.push( - generatePath(ROUTES.DASHBOARD, { - dashboardId: response.payload.uuid, - }), - ); - }, 10); + history.push( + generatePath(ROUTES.DASHBOARD, { + dashboardId: response.payload.uuid, + }), + ); } else if (response.error === 'feature usage exceeded') { setIsFeatureAlert(true); notifications.error({ diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx index 39d3c02a23..03c0ac9912 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -1,37 +1,36 @@ import { ExclamationCircleOutlined } from '@ant-design/icons'; import { Modal } from 'antd'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard'; import { useCallback } from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { DeleteDashboard, DeleteDashboardProps } from 'store/actions'; -import AppActions from 'types/actions'; +import { useQueryClient } from 'react-query'; import { Data } from '../index'; import { TableLinkText } from './styles'; -function DeleteButton({ - deleteDashboard, - id, - refetchDashboardList, -}: DeleteButtonProps): JSX.Element { +function DeleteButton({ id }: Data): JSX.Element { const [modal, contextHolder] = Modal.useModal(); + const queryClient = useQueryClient(); + + const deleteDashboardMutation = useDeleteDashboard(id); + const openConfirmationDialog = useCallback((): void => { modal.confirm({ title: 'Do you really want to delete this dashboard?', icon: , onOk() { - deleteDashboard({ - uuid: id, - refetch: refetchDashboardList, + deleteDashboardMutation.mutateAsync(undefined, { + onSuccess: () => { + queryClient.invalidateQueries([REACT_QUERY_KEY.GET_ALL_DASHBOARDS]); + }, }); }, okText: 'Delete', okButtonProps: { danger: true }, centered: true, }); - }, [modal, deleteDashboard, id, refetchDashboardList]); + }, [modal, deleteDashboardMutation, queryClient]); return ( <> @@ -44,37 +43,12 @@ function DeleteButton({ ); } -interface DispatchProps { - deleteDashboard: ({ - uuid, - }: DeleteDashboardProps) => (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - deleteDashboard: bindActionCreators(DeleteDashboard, dispatch), -}); - -export type DeleteButtonProps = Data & DispatchProps; - -const WrapperDeleteButton = connect(null, mapDispatchToProps)(DeleteButton); - // This is to avoid the type collision function Wrapper(props: Data): JSX.Element { - const { - createdBy, - description, - id, - key, - refetchDashboardList, - lastUpdatedTime, - name, - tags, - } = props; + const { createdBy, description, id, key, lastUpdatedTime, name, tags } = props; return ( - ); diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index 6041b8baf4..87f47e54af 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -17,21 +17,11 @@ import SearchFilter from 'container/ListOfDashboard/SearchFilter'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; -import { - Dispatch, - Key, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import { Key, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { UseQueryResult } from 'react-query'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { generatePath } from 'react-router-dom'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { GET_ALL_DASHBOARD_SUCCESS } from 'types/actions/dashboard'; import { Dashboard } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; import { popupContainer } from 'utils/selectPopupContainer'; @@ -40,9 +30,7 @@ import ImportJSON from './ImportJSON'; import { ButtonContainer, NewDashboardButton, TableContainer } from './styles'; import Createdby from './TableComponents/CreatedBy'; import DateComponent from './TableComponents/Date'; -import DeleteButton, { - DeleteButtonProps, -} from './TableComponents/DeleteButton'; +import DeleteButton from './TableComponents/DeleteButton'; import Name from './TableComponents/Name'; import Tags from './TableComponents/Tags'; @@ -53,7 +41,6 @@ function ListOfAllDashboard(): JSX.Element { refetch: refetchDashboardList, } = useGetAllDashboard(); - const dispatch = useDispatch>(); const { role } = useSelector((state) => state.app); const [action, createNewDashboard, newDashboard] = useComponentPermission( @@ -134,31 +121,12 @@ function ListOfAllDashboard(): JSX.Element { title: 'Action', dataIndex: '', width: 40, - render: ({ - createdBy, - description, - id, - key, - lastUpdatedTime, - name, - tags, - }: DeleteButtonProps) => ( - - ), + render: DeleteButton, }); } return tableColumns; - }, [action, refetchDashboardList]); + }, [action]); const data: Data[] = filteredDashboards?.map((e) => ({ @@ -186,10 +154,6 @@ function ListOfAllDashboard(): JSX.Element { }); if (response.statusCode === 200) { - dispatch({ - type: GET_ALL_DASHBOARD_SUCCESS, - payload: [], - }); history.push( generatePath(ROUTES.DASHBOARD, { dashboardId: response.payload.uuid, @@ -210,7 +174,7 @@ function ListOfAllDashboard(): JSX.Element { errorMessage: (error as AxiosError).toString() || 'Something went Wrong', }); } - }, [newDashboardState, t, dispatch]); + }, [newDashboardState, t]); const getText = useCallback(() => { if (!newDashboardState.error && !newDashboardState.loading) { @@ -352,7 +316,6 @@ export interface Data { createdBy: string; lastUpdatedTime: string; id: string; - refetchDashboardList: UseQueryResult['refetch']; } export default ListOfAllDashboard; diff --git a/frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx b/frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx index 49a071d17e..e47ae535a6 100644 --- a/frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx +++ b/frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx @@ -11,11 +11,11 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import useDebouncedFn from 'hooks/useDebouncedFunction'; import { useEventSourceEvent } from 'hooks/useEventSourceEvent'; import { useNotifications } from 'hooks/useNotifications'; +import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload'; import { useEventSource } from 'providers/EventSource'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { prepareQueryRangePayload } from 'store/actions/dashboard/prepareQueryRangePayload'; import { AppState } from 'store/reducers'; import { ILog } from 'types/api/logs/log'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 28910afbde..17446b0118 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -21,7 +21,12 @@ import { ILog } from 'types/api/logs/log'; import ActionItem, { ActionItemProps } from './ActionItem'; import FieldRenderer from './FieldRenderer'; -import { flattenObject, jsonToDataNodes, recursiveParseJSON } from './utils'; +import { + flattenObject, + jsonToDataNodes, + recursiveParseJSON, + removeEscapeCharacters, +} from './utils'; // Fields which should be restricted from adding it to query const RESTRICTED_FIELDS = ['timestamp']; @@ -58,7 +63,7 @@ function TableView({ .map((key) => ({ key, field: key, - value: JSON.stringify(flattenLogData[key]), + value: removeEscapeCharacters(JSON.stringify(flattenLogData[key])), })); const onTraceHandler = (record: DataType) => (): void => { @@ -164,6 +169,8 @@ function TableView({ width: 70, ellipsis: false, render: (field, record): JSX.Element => { + const textToCopy = field.slice(1, -1); + if (record.field === 'body') { const parsedBody = recursiveParseJSON(field); if (!isEmpty(parsedBody)) { @@ -174,7 +181,7 @@ function TableView({ } return ( - + {field} ); diff --git a/frontend/src/container/LogDetailedView/config.ts b/frontend/src/container/LogDetailedView/config.ts new file mode 100644 index 0000000000..cd34023699 --- /dev/null +++ b/frontend/src/container/LogDetailedView/config.ts @@ -0,0 +1,13 @@ +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +export const typeToArrayTypeMapper: { [key in DataTypes]: DataTypes } = { + [DataTypes.String]: DataTypes.ArrayString, + [DataTypes.Float64]: DataTypes.ArrayFloat64, + [DataTypes.Int64]: DataTypes.ArrayInt64, + [DataTypes.bool]: DataTypes.ArrayBool, + [DataTypes.EMPTY]: DataTypes.EMPTY, + [DataTypes.ArrayFloat64]: DataTypes.ArrayFloat64, + [DataTypes.ArrayInt64]: DataTypes.ArrayInt64, + [DataTypes.ArrayString]: DataTypes.ArrayString, + [DataTypes.ArrayBool]: DataTypes.ArrayBool, +}; diff --git a/frontend/src/container/LogDetailedView/util.test.ts b/frontend/src/container/LogDetailedView/util.test.ts index 4f080e23c1..d5918f2bca 100644 --- a/frontend/src/container/LogDetailedView/util.test.ts +++ b/frontend/src/container/LogDetailedView/util.test.ts @@ -176,8 +176,8 @@ describe('Get Data Types utils', () => { }); // Edge cases - it('should return Int64 for empty array input', () => { - expect(getDataTypes([])).toBe(DataTypes.Int64); + it('should return Empty for empty array input', () => { + expect(getDataTypes([])).toBe(DataTypes.EMPTY); }); it('should handle mixed array (return based on first element)', () => { diff --git a/frontend/src/container/LogDetailedView/utils.tsx b/frontend/src/container/LogDetailedView/utils.tsx index 02890e7dc9..f31534ace8 100644 --- a/frontend/src/container/LogDetailedView/utils.tsx +++ b/frontend/src/container/LogDetailedView/utils.tsx @@ -5,6 +5,7 @@ import { ILog, ILogAggregateAttributesResources } from 'types/api/logs/log'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import BodyTitleRenderer from './BodyTitleRenderer'; +import { typeToArrayTypeMapper } from './config'; import { AnyObject, IFieldAttributes } from './LogDetailedView.types'; export const recursiveParseJSON = (obj: string): Record => { @@ -107,40 +108,6 @@ export function flattenObject(obj: AnyObject, prefix = ''): AnyObject { }, {}); } -const isFloat = (num: number): boolean => num % 1 !== 0; - -export const getDataTypes = (value: unknown): DataTypes => { - if (typeof value === 'string') { - return DataTypes.String; - } - - if (typeof value === 'number') { - return isFloat(value) ? DataTypes.Float64 : DataTypes.Int64; - } - - if (typeof value === 'boolean') { - return DataTypes.bool; - } - - if (Array.isArray(value)) { - const firstElement = value[0]; - - if (typeof firstElement === 'string') { - return DataTypes.ArrayString; - } - - if (typeof firstElement === 'boolean') { - return DataTypes.ArrayBool; - } - - if (typeof firstElement === 'number') { - return isFloat(firstElement) ? DataTypes.ArrayFloat64 : DataTypes.ArrayInt64; - } - } - - return DataTypes.Int64; -}; - export const generateFieldKeyForArray = ( fieldKey: string, dataType: DataTypes, @@ -217,3 +184,59 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => { return JSON.stringify(outputJson, null, 2); }; + +const isFloat = (num: number): boolean => num % 1 !== 0; + +const isBooleanString = (str: string): boolean => + str.toLowerCase() === 'true' || str.toLowerCase() === 'false'; + +const determineType = (val: unknown): DataTypes => { + if (typeof val === 'string') { + if (isBooleanString(val)) { + return DataTypes.bool; + } + + const numberValue = parseFloat(val); + + if (!Number.isNaN(numberValue)) { + return isFloat(numberValue) ? DataTypes.Float64 : DataTypes.Int64; + } + + return DataTypes.String; + } + + if (typeof val === 'number') { + return isFloat(val) ? DataTypes.Float64 : DataTypes.Int64; + } + + if (typeof val === 'boolean') { + return DataTypes.bool; + } + + return DataTypes.EMPTY; +}; + +export const getDataTypes = (value: unknown): DataTypes => { + const getArrayType = (elementType: DataTypes): DataTypes => + typeToArrayTypeMapper[elementType] || DataTypes.EMPTY; + + if (Array.isArray(value)) { + return getArrayType(determineType(value[0])); + } + + return determineType(value); +}; + +export const removeEscapeCharacters = (str: string): string => + str.replace(/\\([ntfr'"\\])/g, (_: string, char: string) => { + const escapeMap: Record = { + n: '\n', + t: '\t', + f: '\f', + r: '\r', + "'": "'", + '"': '"', + '\\': '\\', + }; + return escapeMap[char as keyof typeof escapeMap]; + }); diff --git a/frontend/src/container/LogsExplorerChart/index.tsx b/frontend/src/container/LogsExplorerChart/index.tsx index a64f8eb382..ec329907f3 100644 --- a/frontend/src/container/LogsExplorerChart/index.tsx +++ b/frontend/src/container/LogsExplorerChart/index.tsx @@ -48,7 +48,7 @@ function LogsExplorerChart({ ) : ( ({ description: '', id: v4(), @@ -17,4 +18,5 @@ export const getWidgetQueryBuilder = ({ query, timePreferance: 'GLOBAL_TIME', title, + yAxisUnit, }); diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index 83e6da6db2..15af9981c0 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -1,6 +1,6 @@ import { Col } from 'antd'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph/'; +import Graph from 'container/GridCardLayout/GridCard'; import { databaseCallsAvgDuration, databaseCallsRPS, @@ -65,6 +65,7 @@ function DBCall(): JSX.Element { }, title: GraphTitle.DATABASE_CALLS_RPS, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'reqps', }), [servicename, tagFilterItems], ); @@ -83,6 +84,7 @@ function DBCall(): JSX.Element { }, title: GraphTitle.DATABASE_CALLS_AVG_DURATION, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ms', }), [servicename, tagFilterItems], ); @@ -107,7 +109,6 @@ function DBCall(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, @@ -135,12 +136,12 @@ function DBCall(): JSX.Element { > View Traces + { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index 3abe8d4ca4..cda6e9275c 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -1,6 +1,6 @@ import { Col } from 'antd'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph/'; +import Graph from 'container/GridCardLayout/GridCard'; import { externalCallDuration, externalCallDurationByAddress, @@ -56,6 +56,7 @@ function External(): JSX.Element { }, title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: '%', }), [servicename, tagFilterItems], ); @@ -80,6 +81,7 @@ function External(): JSX.Element { }, title: GraphTitle.EXTERNAL_CALL_DURATION, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ms', }), [servicename, tagFilterItems], ); @@ -100,6 +102,7 @@ function External(): JSX.Element { }, title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'reqps', }), [servicename, tagFilterItems], ); @@ -120,6 +123,7 @@ function External(): JSX.Element { }, title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ms', }), [servicename, tagFilterItems], ); @@ -146,7 +150,6 @@ function External(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, @@ -181,7 +184,6 @@ function External(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, @@ -217,7 +219,6 @@ function External(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, @@ -252,7 +253,6 @@ function External(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index b7ab171ccd..d67053e1e0 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -131,6 +131,7 @@ function Application(): JSX.Element { }, title: GraphTitle.RATE_PER_OPS, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ops', }), [servicename, tagFilterItems, topLevelOperationsRoute], ); @@ -151,6 +152,7 @@ function Application(): JSX.Element { }, title: GraphTitle.ERROR_PERCENTAGE, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: '%', }), [servicename, tagFilterItems, topLevelOperationsRoute], ); @@ -222,7 +224,6 @@ function Application(): JSX.Element { topLevelOperationsIsError={topLevelOperationsIsError} name="operations_per_sec" widget={operationPerSecWidget} - yAxisUnit="ops" opName="Rate" /> @@ -267,7 +268,6 @@ function Application(): JSX.Element { topLevelOperationsIsError={topLevelOperationsIsError} name="error_percentage_%" widget={errorPercentageWidget} - yAxisUnit="%" opName="Error" /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx index b20613bae7..ade8a1bec3 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx @@ -6,8 +6,8 @@ import { apDexToolTipUrlText, } from 'constants/apDex'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph'; -import DisplayThreshold from 'container/GridGraphLayout/WidgetHeader/DisplayThreshold'; +import Graph from 'container/GridCardLayout/GridCard'; +import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThreshold'; import { GraphTitle } from 'container/MetricsApplication/constant'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; @@ -87,7 +87,6 @@ function ApDexMetrics({ widget={apDexMetricsWidget} onDragSelect={onDragSelect} onClickHandler={handleGraphClick('ApDex')} - yAxisUnit="" threshold={threshold} isQueryEnabled={isQueryEnabled} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx index bf6297785f..1b2e5ba0cd 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx @@ -1,7 +1,7 @@ // This component is not been used in the application as we support only metrics for ApDex as of now. // This component is been kept for future reference. import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph'; +import Graph from 'container/GridCardLayout/GridCard'; import { GraphTitle } from 'container/MetricsApplication/constant'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; import { apDexTracesQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; @@ -52,7 +52,6 @@ function ApDexTraces({ widget={apDexTracesWidget} onDragSelect={onDragSelect} onClickHandler={handleGraphClick('ApDex')} - yAxisUnit="" threshold={thresholdValue} isQueryEnabled={isQueryEnabled} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx index 52014ac48b..cb124f545a 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx @@ -1,7 +1,7 @@ import Spinner from 'components/Spinner'; import { FeatureKeys } from 'constants/features'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph/'; +import Graph from 'container/GridCardLayout/GridCard'; import { GraphTitle } from 'container/MetricsApplication/constant'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; @@ -59,6 +59,7 @@ function ServiceOverview({ }, title: GraphTitle.LATENCY, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ns', }), [servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems], ); @@ -93,7 +94,6 @@ function ServiceOverview({ name="service_latency" onDragSelect={onDragSelect} widget={latencyWidget} - yAxisUnit="ns" onClickHandler={handleGraphClick('Service')} isQueryEnabled={isQueryEnabled} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx index 903ff3a15f..f71a0f7301 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx @@ -2,7 +2,7 @@ import { Typography } from 'antd'; import axios from 'axios'; import Spinner from 'components/Spinner'; import { SOMETHING_WENT_WRONG } from 'constants/api'; -import Graph from 'container/GridGraphLayout/Graph/'; +import Graph from 'container/GridCardLayout/GridCard'; import { Card, GraphContainer } from 'container/MetricsApplication/styles'; import { Widgets } from 'types/api/dashboard/getAll'; @@ -17,7 +17,6 @@ function TopLevelOperation({ onDragSelect, handleGraphClick, widget, - yAxisUnit, }: TopLevelOperationProps): JSX.Element { return ( @@ -37,7 +36,6 @@ function TopLevelOperation({ name={name} widget={widget} onClickHandler={handleGraphClick(opName)} - yAxisUnit={yAxisUnit} onDragSelect={onDragSelect} /> )} @@ -56,7 +54,6 @@ interface TopLevelOperationProps { onDragSelect: (start: number, end: number) => void; handleGraphClick: (type: string) => ClickHandlerType; widget: Widgets; - yAxisUnit: string; } export default TopLevelOperation; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx index 4ca191b68c..b0385e2ab0 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx @@ -7,9 +7,7 @@ import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { useNotifications } from 'hooks/useNotifications'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; -import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; -import { isEmpty } from 'lodash-es'; import { ReactNode, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; @@ -58,10 +56,7 @@ function TopOperationMetrics(): JSX.Element { const updatedQuery = useStepInterval(keyOperationWidget.query); - const isEmptyWidget = useMemo( - () => keyOperationWidget.id === 'empty' || isEmpty(keyOperationWidget), - [keyOperationWidget], - ); + const isEmptyWidget = keyOperationWidget.id === PANEL_TYPES.EMPTY_WIDGET; const { data, isLoading } = useGetQueryRange( { @@ -69,7 +64,7 @@ function TopOperationMetrics(): JSX.Element { graphType: keyOperationWidget?.panelTypes, query: updatedQuery, globalSelectedInterval, - variables: getDashboardVariables(), + variables: {}, }, { queryKey: [ diff --git a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx index e753a965a5..4c895716e7 100644 --- a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx +++ b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx @@ -10,7 +10,7 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import { getErrorRate, navigateToTrace } from './utils'; -function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { +function TopOperationsTable({ data }: TopOperationsTableProps): JSX.Element { const { minTime, maxTime } = useSelector( (state) => state.globalTime, ); @@ -20,8 +20,6 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { convertRawQueriesToTraceSelectedTags(queries) || [], ); - const { data } = props; - const params = useParams<{ servicename: string }>(); const handleOnClick = (operation: string): void => { diff --git a/frontend/src/container/MetricsApplication/types.ts b/frontend/src/container/MetricsApplication/types.ts index d9a7251745..f87ce66a2a 100644 --- a/frontend/src/container/MetricsApplication/types.ts +++ b/frontend/src/container/MetricsApplication/types.ts @@ -8,6 +8,7 @@ export interface GetWidgetQueryBuilderProps { query: Widgets['query']; title?: ReactNode; panelTypes: Widgets['panelTypes']; + yAxisUnit?: Widgets['yAxisUnit']; } export interface NavigateToTraceProps { diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index da6e28ccca..c9c1c6dde1 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,67 +1,97 @@ +import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; -import { CSSProperties, useCallback } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { - ToggleAddWidget, - ToggleAddWidgetProps, -} from 'store/actions/dashboard/toggleAddWidget'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import DashboardReducer from 'types/reducer/dashboards'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { CSSProperties } from 'react'; +import { v4 as uuid } from 'uuid'; import menuItems from './menuItems'; import { Card, Container, Text } from './styles'; -function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { - const { dashboards } = useSelector( - (state) => state.dashboards, - ); +function DashboardGraphSlider(): JSX.Element { + const isDarkMode = useIsDarkMode(); - const { pathname } = useLocation(); + const { + handleToggleDashboardSlider, + layouts, + selectedDashboard, + } = useDashboard(); + + const { data } = selectedDashboard || {}; const { notifications } = useNotifications(); - const [selectedDashboard] = dashboards; - const { data } = selectedDashboard; + const updateDashboardMutation = useUpdateDashboard(); - const onClickHandler = useCallback( - (name: PANEL_TYPES) => (): void => { - try { - const emptyLayout = data.layout?.find((e) => e.i === 'empty'); + const onClickHandler = (name: PANEL_TYPES) => (): void => { + const id = uuid(); - if (emptyLayout === undefined) { - notifications.error({ - message: 'Please click on Add Panel Button', + updateDashboardMutation.mutateAsync( + { + uuid: selectedDashboard?.uuid || '', + data: { + title: data?.title || '', + variables: data?.variables || {}, + description: data?.description || '', + name: data?.name || '', + tags: data?.tags || [], + layout: [ + { + i: id, + w: 6, + x: 0, + h: 2, + y: 0, + }, + ...(layouts.filter((layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET) || + []), + ], + widgets: [ + ...(data?.widgets || []), + { + id, + title: '', + description: '', + isStacked: false, + nullZeroValues: '', + opacity: '', + panelTypes: name, + query: initialQueriesMap.metrics, + timePreferance: 'GLOBAL_TIME', + }, + ], + }, + }, + { + onSuccess: (data) => { + if (data.payload) { + handleToggleDashboardSlider(false); + + const queryParams = { + graphType: name, + widgetId: id, + [QueryParams.compositeQuery]: JSON.stringify(initialQueriesMap.metrics), + }; + + history.push( + `${history.location.pathname}/new?${createQueryParams(queryParams)}`, + ); + } + }, + onError: () => { + notifications.success({ + message: SOMETHING_WENT_WRONG, }); - return; - } + }, + }, + ); + }; - toggleAddWidget(false); - - const queryParams = { - graphType: name, - widgetId: emptyLayout.i, - [QueryParams.compositeQuery]: JSON.stringify(initialQueriesMap.metrics), - }; - - history.push(`${pathname}/new?${createQueryParams(queryParams)}`); - } catch (error) { - notifications.error({ - message: 'Something went wrong', - }); - } - }, - [data, toggleAddWidget, notifications, pathname], - ); - const isDarkMode = useIsDarkMode(); const fillColor: CSSProperties['color'] = isDarkMode ? 'white' : 'black'; return ( @@ -76,18 +106,4 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { ); } -interface DispatchProps { - toggleAddWidget: ( - props: ToggleAddWidgetProps, - ) => (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch), -}); - -type Props = DispatchProps; - -export default connect(null, mapDispatchToProps)(DashboardGraphSlider); +export default DashboardGraphSlider; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx index a8f1892fa4..3f6eec23b4 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx @@ -1,33 +1,23 @@ import { SaveOutlined } from '@ant-design/icons'; import { Col, Divider, Input, Space, Typography } from 'antd'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags'; -import { useCallback, useState } from 'react'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; +import { useNotifications } from 'hooks/useNotifications'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { - UpdateDashboardTitleDescriptionTags, - UpdateDashboardTitleDescriptionTagsProps, -} from 'store/actions'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import DashboardReducer from 'types/reducer/dashboards'; import { Button } from './styles'; -function GeneralDashboardSettings({ - updateDashboardTitleDescriptionTags, -}: DescriptionOfDashboardProps): JSX.Element { - const { dashboards } = useSelector( - (state) => state.dashboards, - ); +function GeneralDashboardSettings(): JSX.Element { + const { selectedDashboard, setSelectedDashboard } = useDashboard(); - const [selectedDashboard] = dashboards; - const selectedData = selectedDashboard.data; - const { title } = selectedData; - const { tags } = selectedData; - const { description } = selectedData; + const updateDashboardMutation = useUpdateDashboard(); + + const selectedData = selectedDashboard?.data; + + const { title = '', tags = [], description = '' } = selectedData || {}; const [updatedTitle, setUpdatedTitle] = useState(title); const [updatedTags, setUpdatedTags] = useState(tags || []); @@ -37,27 +27,35 @@ function GeneralDashboardSettings({ const { t } = useTranslation('common'); - const onSaveHandler = useCallback(() => { - const dashboard = selectedDashboard; - // @TODO need to update this function to take title,description,tags only - updateDashboardTitleDescriptionTags({ - dashboard: { - ...dashboard, + const { notifications } = useNotifications(); + + const onSaveHandler = (): void => { + if (!selectedDashboard) return; + + updateDashboardMutation.mutateAsync( + { + ...selectedDashboard, data: { - ...dashboard.data, + ...selectedDashboard.data, description: updatedDescription, tags: updatedTags, title: updatedTitle, }, }, - }); - }, [ - updatedTitle, - updatedTags, - updatedDescription, - selectedDashboard, - updateDashboardTitleDescriptionTags, - ]); + { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + } + }, + onError: () => { + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + }, + }, + ); + }; return ( @@ -83,7 +81,13 @@ function GeneralDashboardSettings({
-
@@ -92,21 +96,4 @@ function GeneralDashboardSettings({ ); } -interface DispatchProps { - updateDashboardTitleDescriptionTags: ( - props: UpdateDashboardTitleDescriptionTagsProps, - ) => (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - updateDashboardTitleDescriptionTags: bindActionCreators( - UpdateDashboardTitleDescriptionTags, - dispatch, - ), -}); - -type DescriptionOfDashboardProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(GeneralDashboardSettings); +export default GeneralDashboardSettings; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index 3f90bf565b..388dffc286 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -1,39 +1,28 @@ import { blue, red } from '@ant-design/colors'; import { PlusOutlined } from '@ant-design/icons'; import { Button, Modal, Row, Space, Tag } from 'antd'; -import { NotificationInstance } from 'antd/es/notification/interface'; import { ResizeTable } from 'components/ResizeTable'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useRef, useState } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; -import DashboardReducer from 'types/reducer/dashboards'; +import { useTranslation } from 'react-i18next'; +import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import { TVariableViewMode } from './types'; import VariableItem from './VariableItem/VariableItem'; -function VariablesSetting({ - updateDashboardVariables, -}: DispatchProps): JSX.Element { +function VariablesSetting(): JSX.Element { const variableToDelete = useRef(null); const [deleteVariableModal, setDeleteVariableModal] = useState(false); - const { dashboards } = useSelector( - (state) => state.dashboards, - ); + const { t } = useTranslation(['dashboard']); + + const { selectedDashboard, setSelectedDashboard } = useDashboard(); const { notifications } = useNotifications(); - const [selectedDashboard] = dashboards; - - const { - data: { variables = {} }, - } = selectedDashboard; + const { variables = {} } = selectedDashboard?.data || {}; const variablesTableData = Object.keys(variables).map((variableName) => ({ key: variableName, @@ -64,6 +53,41 @@ function VariablesSetting({ setVariableViewMode(viewType); }; + const updateMutation = useUpdateDashboard(); + + const updateVariables = ( + updatedVariablesData: Dashboard['data']['variables'], + ): void => { + if (!selectedDashboard) { + return; + } + + updateMutation.mutateAsync( + { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + variables: updatedVariablesData, + }, + }, + { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + notifications.success({ + message: t('variable_updated_successfully'), + }); + } + }, + onError: () => { + notifications.error({ + message: t('error_while_updating_variable'), + }); + }, + }, + ); + }; + const onVariableSaveHandler = ( name: string, variableData: IDashboardVariable, @@ -79,7 +103,7 @@ function VariablesSetting({ if (oldName) { delete newVariables[oldName]; } - updateDashboardVariables(newVariables, notifications); + updateVariables(newVariables); onDoneVariableViewMode(); }; @@ -91,7 +115,7 @@ function VariablesSetting({ const handleDeleteConfirm = (): void => { const newVariables = { ...variables }; if (variableToDelete?.current) delete newVariables[variableToDelete?.current]; - updateDashboardVariables(newVariables, notifications); + updateVariables(newVariables); variableToDelete.current = null; setDeleteVariableModal(false); }; @@ -182,20 +206,4 @@ function VariablesSetting({ ); } -interface DispatchProps { - updateDashboardVariables: ( - props: Record, - notify: NotificationInstance, - ) => (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - updateDashboardVariables: bindActionCreators( - UpdateDashboardVariables, - dispatch, - ), -}); - -export default connect(null, mapDispatchToProps)(VariablesSetting); +export default VariablesSetting; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx index 50a69495fa..5a1bc6afac 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx @@ -3,12 +3,12 @@ import { Tabs } from 'antd'; import GeneralDashboardSettings from './General'; import VariablesSetting from './Variables'; -function DashboardSettingsContent(): JSX.Element { - const items = [ - { label: 'General', key: 'general', children: }, - { label: 'Variables', key: 'variables', children: }, - ]; +const items = [ + { label: 'General', key: 'general', children: }, + { label: 'Variables', key: 'variables', children: }, +]; +function DashboardSettingsContent(): JSX.Element { return ; } diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx index d318e684f8..561af111ae 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx @@ -1,34 +1,25 @@ import { Row } from 'antd'; -import { NotificationInstance } from 'antd/es/notification/interface'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; import { map, sortBy } from 'lodash-es'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useState } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables'; +import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; import VariableItem from './VariableItem'; -function DashboardVariableSelection({ - updateDashboardVariables, -}: DispatchProps): JSX.Element { - const { dashboards } = useSelector( - (state) => state.dashboards, - ); - const [selectedDashboard] = dashboards; - const { - data: { variables = {} }, - } = selectedDashboard; +function DashboardVariableSelection(): JSX.Element | null { + const { selectedDashboard, setSelectedDashboard } = useDashboard(); + + const { data } = selectedDashboard || {}; + + const { variables } = data || {}; const [update, setUpdate] = useState(false); const [lastUpdatedVar, setLastUpdatedVar] = useState(''); - const { notifications } = useNotifications(); const { role } = useSelector((state) => state.app); @@ -37,6 +28,42 @@ function DashboardVariableSelection({ setUpdate(!update); }; + const updateMutation = useUpdateDashboard(); + const { notifications } = useNotifications(); + + const updateVariables = ( + updatedVariablesData: Dashboard['data']['variables'], + ): void => { + if (!selectedDashboard) { + return; + } + + updateMutation.mutateAsync( + { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + variables: updatedVariablesData, + }, + }, + { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + notifications.success({ + message: 'Variable updated successfully', + }); + } + }, + onError: () => { + notifications.error({ + message: 'Error while updating variable', + }); + }, + }, + ); + }; + const onValueUpdate = ( name: string, value: IDashboardVariable['selectedValue'], @@ -44,8 +71,8 @@ function DashboardVariableSelection({ const updatedVariablesData = { ...variables }; updatedVariablesData[name].selectedValue = value; - if (role !== 'VIEWER') { - updateDashboardVariables(updatedVariablesData, notifications); + if (role !== 'VIEWER' && selectedDashboard) { + updateVariables(updatedVariablesData); } onVarChanged(name); @@ -58,13 +85,17 @@ function DashboardVariableSelection({ updatedVariablesData[name].allSelected = value; if (role !== 'VIEWER') { - updateDashboardVariables(updatedVariablesData, notifications); + updateVariables(updatedVariablesData); } onVarChanged(name); }; + if (!variables) { + return null; + } + return ( - + {map(sortBy(Object.keys(variables)), (variableName) => ( ))} @@ -83,20 +114,4 @@ function DashboardVariableSelection({ ); } -interface DispatchProps { - updateDashboardVariables: ( - props: Parameters[0], - notify: NotificationInstance, - ) => (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - updateDashboardVariables: bindActionCreators( - UpdateDashboardVariables, - dispatch, - ), -}); - -export default connect(null, mapDispatchToProps)(DashboardVariableSelection); +export default DashboardVariableSelection; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx index 72a10a3f30..fc51efd69a 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx @@ -6,7 +6,7 @@ import DashboardSettingsContent from '../DashboardSettings'; import { DrawerContainer } from './styles'; function SettingsDrawer(): JSX.Element { - const [visible, setVisible] = useState(false); // TODO Make it False + const [visible, setVisible] = useState(false); const showDrawer = (): void => { setVisible(true); @@ -25,7 +25,7 @@ function SettingsDrawer(): JSX.Element { placement="right" width="70%" onClose={onClose} - visible={visible} + open={visible} > diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx index 2faafd7cbb..3c6ca326a3 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx @@ -1,25 +1,22 @@ import { ShareAltOutlined } from '@ant-design/icons'; import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd'; import useComponentPermission from 'hooks/useComponentPermission'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; import DashboardVariableSelection from '../DashboardVariablesSelection'; import SettingsDrawer from './SettingsDrawer'; import ShareModal from './ShareModal'; function DescriptionOfDashboard(): JSX.Element { - const { dashboards } = useSelector( - (state) => state.dashboards, - ); + const { selectedDashboard } = useDashboard(); - const [selectedDashboard] = dashboards; - const selectedData = selectedDashboard.data; - const { title, tags, description } = selectedData; + const selectedData = selectedDashboard?.data; + const { title, tags, description } = selectedData || {}; const [isJSONModalVisible, isIsJSONModalVisible] = useState(false); @@ -34,26 +31,29 @@ function DescriptionOfDashboard(): JSX.Element { return ( - + {title} {description} +
- {tags?.map((e) => ( - {e} + {tags?.map((tag) => ( + {tag} ))}
+ - + {selectedData && ( + + )} + {editDashboard && } + + ); + } + + return ( + + ); +} + +export interface LogsProcessingSimulatorProps { + inputLogs: ILog[]; + pipeline: PipelineData; +} + +export default LogsProcessingSimulator; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/components/PipelineSimulationResult/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/components/PipelineSimulationResult/index.tsx new file mode 100644 index 0000000000..438d529e5a --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/components/PipelineSimulationResult/index.tsx @@ -0,0 +1,43 @@ +import './styles.scss'; + +import { ILog } from 'types/api/logs/log'; +import { PipelineData } from 'types/api/pipeline/def'; + +import LogsList from '../../../components/LogsList'; +import usePipelinePreview from '../../../hooks/usePipelinePreview'; + +function PipelineSimulationResult({ + inputLogs, + pipeline, +}: PipelineSimulationResultProps): JSX.Element { + const { isLoading, outputLogs, isError, errorMsg } = usePipelinePreview({ + pipeline, + inputLogs, + }); + + if (isError) { + return ( +
+
There was an error
+
{errorMsg}
+
+ ); + } + + if (isLoading) { + return
Loading...
; + } + + if (outputLogs.length < 1) { + return
No logs found
; + } + + return ; +} + +export interface PipelineSimulationResultProps { + inputLogs: ILog[]; + pipeline: PipelineData; +} + +export default PipelineSimulationResult; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/components/PipelineSimulationResult/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/components/PipelineSimulationResult/styles.scss new file mode 100644 index 0000000000..0bdbb213ea --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/components/PipelineSimulationResult/styles.scss @@ -0,0 +1,3 @@ +.pipeline-simulation-error { + text-align: center; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/index.tsx new file mode 100644 index 0000000000..26d67d0c5a --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/index.tsx @@ -0,0 +1,55 @@ +import './styles.scss'; + +import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelection/config'; +import { useState } from 'react'; +import { PipelineData } from 'types/api/pipeline/def'; + +import PreviewIntervalSelector from '../components/PreviewIntervalSelector'; +import SampleLogsResponseDisplay from '../components/SampleLogs/SampleLogsResponseDisplay'; +import useSampleLogs from '../hooks/useSampleLogs'; +import LogsProcessingSimulator from './components/LogsProcessingSimulator'; + +function PipelineProcessingPreview({ + pipeline, +}: PipelineProcessingPreviewProps): JSX.Element { + const last1HourInterval = RelativeDurationOptions[3].value; + const [logsSampleQueryInterval, setLogsSampleQueryInterval] = useState( + last1HourInterval, + ); + + const sampleLogsResponse = useSampleLogs({ + filter: pipeline.filter, + timeInterval: logsSampleQueryInterval, + count: 5, + }); + + const { logs: sampleLogs } = sampleLogsResponse; + + return ( +
+
+
Sample logs
+ +
+
+ +
+
+
Processed Output
+
+
+ +
+
+ ); +} + +export interface PipelineProcessingPreviewProps { + pipeline: PipelineData; +} + +export default PipelineProcessingPreview; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/styles.scss new file mode 100644 index 0000000000..d1e263242f --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview/styles.scss @@ -0,0 +1,19 @@ +.pipeline-preview-section-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 0.4rem; + margin: 1.2rem 0 0.4rem 0; +} + +.pipeline-preview-logs-container { + position: relative; + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 12em; + overflow: hidden; + border: 1px solid rgba(253, 253, 253, 0.12); +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx new file mode 100644 index 0000000000..122d5e2b3a --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx @@ -0,0 +1,52 @@ +import './styles.scss'; + +import { ExpandAltOutlined } from '@ant-design/icons'; +import LogDetail from 'components/LogDetail'; +import dayjs from 'dayjs'; +import { useActiveLog } from 'hooks/logs/useActiveLog'; +import { ILog } from 'types/api/logs/log'; + +function LogsList({ logs }: LogsListProps): JSX.Element { + const { + activeLog, + onSetActiveLog, + onClearActiveLog, + onAddToQuery, + } = useActiveLog(); + + const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log); + + return ( +
+ {logs.map((log) => ( +
+
+ {dayjs(String(log.timestamp)).format('MMM DD HH:mm:ss.SSS')} +
+
{log.body}
+
+ +
+
+ ))} + +
+ ); +} + +interface LogsListProps { + logs: ILog[]; +} + +export default LogsList; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/styles.scss new file mode 100644 index 0000000000..80cec9ef28 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/styles.scss @@ -0,0 +1,46 @@ +.logs-preview-list-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: stretch; + + box-sizing: border-box; + padding: 0.25rem 0.5rem; +} + +.logs-preview-list-item { + width: 100%; + position: relative; + + display: flex; + justify-content: space-between; + align-items: center; + + flex-grow: 1; +} + +.logs-preview-list-item:not(:first-child) { + border-top: 1px solid rgba(253, 253, 253, 0.12); +} + +.logs-preview-list-item-timestamp { + margin-right: 0.75rem; + white-space: nowrap; +} + +.logs-preview-list-item-body { + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.logs-preview-list-item-expand{ + margin-left: 0.75rem; + color: #1677ff; + padding: 0.25rem 0.375rem; + cursor: pointer; + font-size: 12px; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/index.tsx new file mode 100644 index 0000000000..63ee3ff3c0 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/index.tsx @@ -0,0 +1,55 @@ +import './styles.scss'; + +import { + initialFilters, + initialQueriesMap, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { Time } from 'container/TopNav/DateTimeSelection/config'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import cloneDeep from 'lodash-es/cloneDeep'; +import { useMemo } from 'react'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; + +function LogsCountInInterval({ + filter, + timeInterval, +}: LogsCountInIntervalProps): JSX.Element | null { + const query = useMemo(() => { + const q = cloneDeep(initialQueriesMap.logs); + q.builder.queryData[0] = { + ...q.builder.queryData[0], + filters: filter || initialFilters, + aggregateOperator: LogsAggregatorOperator.COUNT, + }; + return q; + }, [filter]); + + const result = useGetQueryRange({ + graphType: PANEL_TYPES.TABLE, + query, + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval: timeInterval, + }); + + if (!result.isFetched) { + return null; + } + + const count = + result?.data?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0] + ?.values?.[0]?.value; + return ( +
+ {count} matches in +
+ ); +} + +interface LogsCountInIntervalProps { + filter: TagFilter; + timeInterval: Time; +} + +export default LogsCountInInterval; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/styles.scss new file mode 100644 index 0000000000..2074e176a6 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/styles.scss @@ -0,0 +1,3 @@ +.logs-filter-preview-matched-logs-count { + margin-right: 0.5rem; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/index.tsx new file mode 100644 index 0000000000..a40f1d0376 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/index.tsx @@ -0,0 +1,45 @@ +import './styles.scss'; + +import { Select } from 'antd'; +import { + RelativeDurationOptions, + Time, +} from 'container/TopNav/DateTimeSelection/config'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; + +import LogsCountInInterval from './components/LogsCountInInterval'; + +function PreviewIntervalSelector({ + previewFilter, + value, + onChange, +}: PreviewIntervalSelectorProps): JSX.Element { + const onSelectInterval = (value: unknown): void => onChange(value as Time); + + const isEmptyFilter = (previewFilter?.items?.length || 0) < 1; + + return ( +
+ {!isEmptyFilter && ( + + )} +
+ +
+
+ ); +} + +interface PreviewIntervalSelectorProps { + value: Time; + onChange: (interval: Time) => void; + previewFilter: TagFilter; +} + +export default PreviewIntervalSelector; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/styles.scss new file mode 100644 index 0000000000..d2bc3347ea --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/styles.scss @@ -0,0 +1,4 @@ +.logs-filter-preview-time-interval-summary { + display: flex; + align-items: center; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/SampleLogsResponseDisplay.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/SampleLogsResponseDisplay.tsx new file mode 100644 index 0000000000..e0aacf2bac --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/SampleLogsResponseDisplay.tsx @@ -0,0 +1,32 @@ +import { SampleLogsResponse } from '../../hooks/useSampleLogs'; +import LogsList from '../LogsList'; + +function SampleLogsResponseDisplay({ + response, +}: SampleLogsResponseDisplayProps): JSX.Element { + const { isLoading, isError, logs } = response; + + if (isError) { + return ( +
+ An error occured while querying sample logs +
+ ); + } + + if (isLoading) { + return
Loading...
; + } + + if (logs.length < 1) { + return
No logs found
; + } + + return ; +} + +export interface SampleLogsResponseDisplayProps { + response: SampleLogsResponse; +} + +export default SampleLogsResponseDisplay; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/index.tsx new file mode 100644 index 0000000000..5b2a6b7b41 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/index.tsx @@ -0,0 +1,16 @@ +import useSampleLogs, { SampleLogsRequest } from '../../hooks/useSampleLogs'; +import LogsResponseDisplay from './SampleLogsResponseDisplay'; + +function SampleLogs(props: SampleLogsRequest): JSX.Element { + const sampleLogsResponse = useSampleLogs(props); + + if ((props?.filter?.items?.length || 0) < 1) { + return ( +
Please select a filter
+ ); + } + + return ; +} + +export default SampleLogs; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/styles.scss new file mode 100644 index 0000000000..815a8ce45f --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/styles.scss @@ -0,0 +1,7 @@ +.sample-logs-notice-container { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/usePipelinePreview.ts b/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/usePipelinePreview.ts new file mode 100644 index 0000000000..ad875cdcd3 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/usePipelinePreview.ts @@ -0,0 +1,53 @@ +import simulatePipelineProcessing, { + PipelineSimulationResponse, +} from 'api/pipeline/preview'; +import { AxiosError } from 'axios'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useQuery } from 'react-query'; +import { ILog } from 'types/api/logs/log'; +import { PipelineData } from 'types/api/pipeline/def'; + +export interface PipelinePreviewRequest { + pipeline: PipelineData; + inputLogs: ILog[]; +} + +export interface PipelinePreviewResponse { + isLoading: boolean; + outputLogs: ILog[]; + isError: boolean; + errorMsg: string; +} + +const usePipelinePreview = ({ + pipeline, + inputLogs, +}: PipelinePreviewRequest): PipelinePreviewResponse => { + // Ensure log timestamps are numbers for pipeline preview API request + // ILog allows both number and string while the API needs a number + const simulationInput = inputLogs.map((l) => ({ + ...l, + timestamp: new Date(l.timestamp).getTime(), + })); + + const response = useQuery({ + queryFn: async () => + simulatePipelineProcessing({ + logs: simulationInput, + pipelines: [pipeline], + }), + queryKey: [REACT_QUERY_KEY.LOGS_PIPELINE_PREVIEW, pipeline, inputLogs], + retry: false, + }); + + const { isFetching, isError, data, error } = response; + + return { + isLoading: isFetching, + outputLogs: data?.logs || [], + isError, + errorMsg: error?.response?.data?.error || '', + }; +}; + +export default usePipelinePreview; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/useSampleLogs.ts b/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/useSampleLogs.ts new file mode 100644 index 0000000000..3789856771 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/useSampleLogs.ts @@ -0,0 +1,69 @@ +import { + initialFilters, + initialQueriesMap, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { Time } from 'container/TopNav/DateTimeSelection/config'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import cloneDeep from 'lodash-es/cloneDeep'; +import { useMemo } from 'react'; +import { ILog } from 'types/api/logs/log'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; + +export interface SampleLogsRequest { + filter: TagFilter; + timeInterval: Time; + count: number; +} + +export interface SampleLogsResponse { + isLoading: boolean; + logs: ILog[]; + isError: boolean; +} + +const DEFAULT_SAMPLE_LOGS_COUNT = 5; + +const useSampleLogs = ({ + filter, + timeInterval, + count, +}: SampleLogsRequest): SampleLogsResponse => { + const query = useMemo(() => { + const q = cloneDeep(initialQueriesMap.logs); + q.builder.queryData[0] = { + ...q.builder.queryData[0], + filters: filter || initialFilters, + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + limit: count || DEFAULT_SAMPLE_LOGS_COUNT, + }; + return q; + }, [count, filter]); + + const response = useGetQueryRange({ + graphType: PANEL_TYPES.LIST, + query, + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval: timeInterval, + }); + + const { isFetching: isLoading, data } = response; + + const errorMsg = data?.error || ''; + const isError = response.isError || Boolean(errorMsg); + + let logs: ILog[] = []; + if (!(isLoading || isError)) { + const logsList = data?.payload?.data?.newResult?.data?.result[0]?.list || []; + logs = logsList.map((item) => ({ + ...item.data, + timestamp: item.timestamp, + })); + } + + return { isLoading, logs, isError }; +}; + +export default useSampleLogs; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions.tsx deleted file mode 100644 index 1f86d675e8..0000000000 --- a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { IconListStyle } from '../styles'; -import DeleteAction from './TableActions/DeleteAction'; -import EditAction from './TableActions/EditAction'; -// import ViewAction from './TableActions/ViewAction'; - -function PipelineActions({ - isPipelineAction, - editAction, - deleteAction, -}: PipelineActionsProps): JSX.Element { - return ( - - - {/* */} - - - ); -} - -export interface PipelineActionsProps { - isPipelineAction: boolean; - editAction: VoidFunction; - deleteAction: VoidFunction; -} -export default PipelineActions; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions/components/PreviewAction.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions/components/PreviewAction.tsx new file mode 100644 index 0000000000..353feec486 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions/components/PreviewAction.tsx @@ -0,0 +1,44 @@ +import { EyeFilled } from '@ant-design/icons'; +import { Divider, Modal } from 'antd'; +import PipelineProcessingPreview from 'container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview'; +import { useState } from 'react'; +import { PipelineData } from 'types/api/pipeline/def'; + +import { iconStyle } from '../../../config'; + +function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null { + const [previewKey, setPreviewKey] = useState(null); + const isModalOpen = Boolean(previewKey); + + const openModal = (): void => setPreviewKey(String(Math.random())); + const closeModal = (): void => setPreviewKey(null); + + // Can only preview pipelines with some processors in them + if ((pipeline?.config?.length || 0) < 1) { + return null; + } + + return ( + <> + + + + {isModalOpen && ( + + )} + + + ); +} + +export interface PreviewActionProps { + pipeline: PipelineData; +} +export default PreviewAction; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions/index.tsx new file mode 100644 index 0000000000..b6b41cf9f7 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions/index.tsx @@ -0,0 +1,27 @@ +import { PipelineData } from 'types/api/pipeline/def'; + +import { IconListStyle } from '../../styles'; +import DeleteAction from '../TableActions/DeleteAction'; +import EditAction from '../TableActions/EditAction'; +import PreviewAction from './components/PreviewAction'; + +function PipelineActions({ + pipeline, + editAction, + deleteAction, +}: PipelineActionsProps): JSX.Element { + return ( + + + + + + ); +} + +export interface PipelineActionsProps { + pipeline: PipelineData; + editAction: VoidFunction; + deleteAction: VoidFunction; +} +export default PipelineActions; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/index.tsx similarity index 72% rename from frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/index.tsx rename to frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/index.tsx index b33ed6c087..6217f168b0 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/index.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/index.tsx @@ -3,9 +3,9 @@ import './styles.scss'; import { queryFilterTags } from 'hooks/queryBuilder/useTag'; import { PipelineData } from 'types/api/pipeline/def'; -function PipelineFilterPreview({ +function PipelineFilterSummary({ filter, -}: PipelineFilterPreviewProps): JSX.Element { +}: PipelineFilterSummaryProps): JSX.Element { return (
{queryFilterTags(filter).map((tag) => ( @@ -17,8 +17,8 @@ function PipelineFilterPreview({ ); } -interface PipelineFilterPreviewProps { +interface PipelineFilterSummaryProps { filter: PipelineData['filter']; } -export default PipelineFilterPreview; +export default PipelineFilterSummary; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/styles.scss similarity index 100% rename from frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/styles.scss rename to frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/styles.scss diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/ProcessorActions.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/ProcessorActions.tsx new file mode 100644 index 0000000000..fc79a94e9c --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/ProcessorActions.tsx @@ -0,0 +1,21 @@ +import { IconListStyle } from '../styles'; +import DeleteAction from './TableActions/DeleteAction'; +import EditAction from './TableActions/EditAction'; + +function ProcessorActions({ + editAction, + deleteAction, +}: ProcessorActionsProps): JSX.Element { + return ( + + + + + ); +} + +export interface ProcessorActionsProps { + editAction: VoidFunction; + deleteAction: VoidFunction; +} +export default ProcessorActions; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/ViewAction.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/ViewAction.tsx deleted file mode 100644 index 0260114a81..0000000000 --- a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/ViewAction.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { CopyFilled, EyeFilled } from '@ant-design/icons'; - -import { iconStyle, smallIconStyle } from '../../config'; - -function ViewAction({ isPipelineAction }: ViewActionProps): JSX.Element { - if (isPipelineAction) { - return ; - } - return ( - - - - ); -} - -export interface ViewActionProps { - isPipelineAction: boolean; -} -export default ViewAction; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx index 25ec206566..8fc4b5b6eb 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx @@ -4,7 +4,7 @@ import { PipelineData, ProcessorData } from 'types/api/pipeline/def'; import { PipelineIndexIcon } from '../AddNewProcessor/styles'; import { ColumnDataStyle, ListDataStyle, ProcessorIndexIcon } from '../styles'; -import PipelineFilterPreview from './PipelineFilterPreview'; +import PipelineFilterSummary from './PipelineFilterSummary'; const componentMap: ComponentMap = { orderId: ({ record }) => {record}, @@ -15,7 +15,7 @@ const componentMap: ComponentMap = { ), id: ({ record }) => {record}, name: ({ record }) => {record}, - filter: ({ record }) => , + filter: ({ record }) => , }; function TableComponents({ diff --git a/frontend/src/container/PipelinePage/PipelineListsView/config.ts b/frontend/src/container/PipelinePage/PipelineListsView/config.ts index baecbb8d14..6fc56c48b5 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/config.ts +++ b/frontend/src/container/PipelinePage/PipelineListsView/config.ts @@ -14,25 +14,25 @@ import NameInput from './AddNewPipeline/FormFields/NameInput'; export const pipelineFields = [ { id: 1, - fieldName: 'Filter', - placeholder: 'pipeline_filter_placeholder', - name: 'filter', - component: FilterInput, - }, - { - id: 2, fieldName: 'Name', placeholder: 'pipeline_name_placeholder', name: 'name', component: NameInput, }, { - id: 4, + id: 2, fieldName: 'Description', placeholder: 'pipeline_description_placeholder', name: 'description', component: DescriptionTextArea, }, + { + id: 3, + fieldName: 'Filter', + placeholder: 'pipeline_filter_placeholder', + name: 'filter', + component: FilterInput, + }, ]; export const tagInputStyle: React.CSSProperties = { diff --git a/frontend/src/container/PipelinePage/PipelineListsView/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/index.tsx index 703088ad22..00a512d3e0 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/index.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/index.tsx @@ -172,7 +172,7 @@ function PipelineListsView({ align: 'center', render: (_value, record): JSX.Element => ( diff --git a/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx b/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx index 973432f722..83f503107b 100644 --- a/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx +++ b/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx @@ -1,11 +1,13 @@ import { render } from '@testing-library/react'; -import PipelineActions from 'container/PipelinePage/PipelineListsView/TableComponents/PipelineActions'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import i18n from 'ReactI18'; import store from 'store'; +import { pipelineMockData } from '../mocks/pipeline'; +import PipelineActions from '../PipelineListsView/TableComponents/PipelineActions'; + describe('PipelinePage container test', () => { it('should render PipelineActions section', () => { const { asFragment } = render( @@ -13,7 +15,7 @@ describe('PipelinePage container test', () => { diff --git a/frontend/src/container/PipelinePage/tests/ViewAction.test.tsx b/frontend/src/container/PipelinePage/tests/ViewAction.test.tsx deleted file mode 100644 index 31731adf5b..0000000000 --- a/frontend/src/container/PipelinePage/tests/ViewAction.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { render } from '@testing-library/react'; -import ViewAction from 'container/PipelinePage/PipelineListsView/TableComponents/TableActions/ViewAction'; -import { I18nextProvider } from 'react-i18next'; -import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; -import i18n from 'ReactI18'; -import store from 'store'; - -describe('PipelinePage container test', () => { - it('should render ViewAction section', () => { - const { asFragment } = render( - - - - - - - , - ); - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineActions.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineActions.test.tsx.snap index e96d3ac9a1..b59d850143 100644 --- a/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineActions.test.tsx.snap +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineActions.test.tsx.snap @@ -17,6 +17,27 @@ exports[`PipelinePage container test should render PipelineActions section 1`] =
+ + + - - - - -`; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index 4b2c4fe6b3..59c693b6e6 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -87,10 +87,7 @@ function QueryBuilderSearch({ handleSearch(value); }; - const isDisabled = - !!searchValue || - OPERATORS.HAS === tagOperator || - OPERATORS.NHAS === tagOperator; + const isDisabled = !!searchValue; return ( diff --git a/frontend/src/container/ServiceApplication/types.ts b/frontend/src/container/ServiceApplication/types.ts index 6b800216be..6ff6b7c87a 100644 --- a/frontend/src/container/ServiceApplication/types.ts +++ b/frontend/src/container/ServiceApplication/types.ts @@ -1,7 +1,7 @@ import { ServiceDataProps } from 'api/metrics/getTopLevelOperations'; import { Time } from 'container/TopNav/DateTimeSelection/config'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import { UseQueryResult } from 'react-query'; -import { GetQueryResultsProps } from 'store/actions/dashboard/getQueryResults'; import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { ServicesList } from 'types/api/metrics/getService'; diff --git a/frontend/src/container/ServiceApplication/utils.ts b/frontend/src/container/ServiceApplication/utils.ts index d4d1b34bce..efe3a9ab2e 100644 --- a/frontend/src/container/ServiceApplication/utils.ts +++ b/frontend/src/container/ServiceApplication/utils.ts @@ -2,7 +2,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; -import { GetQueryResultsProps } from 'store/actions/dashboard/getQueryResults'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import { ServicesList } from 'types/api/metrics/getService'; import { QueryDataV3 } from 'types/api/widgets/getQuery'; import { EQueryType } from 'types/common/dashboard'; diff --git a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx index 6824b23995..8594ccd90c 100644 --- a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx +++ b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx @@ -32,7 +32,7 @@ function TimeSeriesView({ {!isLoading && !isError && ( { if (route === ROUTES.SERVICE_MAP) { - return ServiceMapOptions[2].value; + return RelativeDurationOptions[2].value; } if (route === ROUTES.APPLICATION) { return Options[2].value; @@ -63,7 +63,7 @@ export const getDefaultOption = (route: string): Time => { export const getOptions = (routes: string): Option[] => { if (routes === ROUTES.SERVICE_MAP) { - return ServiceMapOptions; + return RelativeDurationOptions; } return Options; }; @@ -78,6 +78,7 @@ export const routesToSkip = [ ROUTES.VERSION, ROUTES.ALL_DASHBOARD, ROUTES.ORG_SETTINGS, + ROUTES.INGESTION_SETTINGS, ROUTES.ERROR_DETAIL, ROUTES.ALERTS_NEW, ROUTES.EDIT_ALERTS, diff --git a/frontend/src/hooks/dashboard/useDeleteDashboard.tsx b/frontend/src/hooks/dashboard/useDeleteDashboard.tsx new file mode 100644 index 0000000000..35a3c27fce --- /dev/null +++ b/frontend/src/hooks/dashboard/useDeleteDashboard.tsx @@ -0,0 +1,15 @@ +import deleteDashboard from 'api/dashboard/delete'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useMutation, UseMutationResult } from 'react-query'; +import { PayloadProps } from 'types/api/dashboard/delete'; + +export const useDeleteDashboard = ( + id: string, +): UseMutationResult => + useMutation({ + mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD, + mutationFn: () => + deleteDashboard({ + uuid: id, + }), + }); diff --git a/frontend/src/hooks/dashboard/useUpdateDashboard.tsx b/frontend/src/hooks/dashboard/useUpdateDashboard.tsx index b4c34974dc..cdab00bfb2 100644 --- a/frontend/src/hooks/dashboard/useUpdateDashboard.tsx +++ b/frontend/src/hooks/dashboard/useUpdateDashboard.tsx @@ -1,10 +1,21 @@ import update from 'api/dashboard/update'; +import dayjs from 'dayjs'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useMutation, UseMutationResult } from 'react-query'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { Dashboard } from 'types/api/dashboard/getAll'; import { Props } from 'types/api/dashboard/update'; -export const useUpdateDashboard = (): UseUpdateDashboard => useMutation(update); +export const useUpdateDashboard = (): UseUpdateDashboard => { + const { updatedTimeRef } = useDashboard(); + return useMutation(update, { + onSuccess: (data) => { + if (data.payload) { + updatedTimeRef.current = dayjs(data.payload.updated_at); + } + }, + }); +}; type UseUpdateDashboard = UseMutationResult< SuccessResponse | ErrorResponse, diff --git a/frontend/src/hooks/queryBuilder/useGetQueriesRange.ts b/frontend/src/hooks/queryBuilder/useGetQueriesRange.ts index c9f78d6e80..d7ec04d336 100644 --- a/frontend/src/hooks/queryBuilder/useGetQueriesRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetQueriesRange.ts @@ -1,4 +1,8 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { + GetMetricQueryRange, + GetQueryResultsProps, +} from 'lib/dashboard/getQueryResults'; import { useMemo } from 'react'; import { QueryKey, @@ -6,10 +10,6 @@ import { 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'; diff --git a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts index b6e12b517c..9aa76405c2 100644 --- a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts @@ -1,10 +1,10 @@ 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'; +} from 'lib/dashboard/getQueryResults'; +import { useMemo } from 'react'; +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; diff --git a/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts index 28bc07566f..f3ecd21101 100644 --- a/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts @@ -1,9 +1,10 @@ import { initialQueriesMap } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; 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'; @@ -26,13 +27,15 @@ export const useGetWidgetQueryRange = ( const { stagedQuery } = useQueryBuilder(); + const { selectedDashboard } = useDashboard(); + return useGetQueryRange( { graphType, selectedTime, globalSelectedInterval, query: stagedQuery || initialQueriesMap.metrics, - variables: getDashboardVariables(), + variables: getDashboardVariables(selectedDashboard?.data.variables), }, { enabled: !!stagedQuery, diff --git a/frontend/src/hooks/useChartMutable.ts b/frontend/src/hooks/useChartMutable.ts index 157cae13c5..642b294b53 100644 --- a/frontend/src/hooks/useChartMutable.ts +++ b/frontend/src/hooks/useChartMutable.ts @@ -1,5 +1,5 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; -import { PanelTypeAndGraphManagerVisibilityProps } from 'container/GridGraphLayout/Graph/FullView/types'; +import { PanelTypeAndGraphManagerVisibilityProps } from 'container/GridCardLayout/GridCard/FullView/types'; import { PanelTypeKeys } from 'types/common/queryBuilder'; export const useChartMutable = ({ diff --git a/frontend/src/hooks/useTabFocus.tsx b/frontend/src/hooks/useTabFocus.tsx new file mode 100644 index 0000000000..8476d9d116 --- /dev/null +++ b/frontend/src/hooks/useTabFocus.tsx @@ -0,0 +1,28 @@ +import { useEffect, useRef, useState } from 'react'; + +const useTabVisibility = (): boolean => { + const [isVisible, setIsVisible] = useState(false); + const prevVisibilityRef = useRef(isVisible); + + useEffect(() => { + const handleVisibilityChange = (): void => { + const isTabVisible = document.visibilityState === 'visible'; + if (isTabVisible && !prevVisibilityRef.current) { + setIsVisible(true); + } else if (!isTabVisible && prevVisibilityRef.current) { + setIsVisible(false); + } + prevVisibilityRef.current = isTabVisible; + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return (): void => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, []); + + return isVisible; +}; + +export default useTabVisibility; diff --git a/frontend/src/index.html.ejs b/frontend/src/index.html.ejs index abfd8258bb..227506006b 100644 --- a/frontend/src/index.html.ejs +++ b/frontend/src/index.html.ejs @@ -173,14 +173,20 @@ t.type = 'text/javascript'; t.async = !0; t.src = - 'https://cdn.segment.com/analytics.js/v1/' + key + '/analytics.min.js'; + 'https://analytics-cdn.signoz.io/analytics.js/v1/' + + key + + '/analytics.min.js'; var n = document.getElementsByTagName('script')[0]; n.parentNode.insertBefore(t, n); analytics._loadOptions = i; }; analytics._writeKey = SEGMENT_ID; analytics.SNIPPET_VERSION = '4.16.1'; - analytics.load(SEGMENT_ID); + analytics.load(SEGMENT_ID, { + integrations: { + 'Segment.io': { apiHost: 'analytics-api.signoz.io/v1' }, + }, + }); analytics.page(); } })(); diff --git a/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts index 1fae298675..346cd61ff6 100644 --- a/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts +++ b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts @@ -1,33 +1,29 @@ import getStartEndRangeTime from 'lib/getStartEndRangeTime'; import store from 'store'; +import { Dashboard } from 'types/api/dashboard/getAll'; -export const getDashboardVariables = (): Record => { - try { - const { - globalTime, - dashboards: { dashboards }, - } = store.getState(); - if (dashboards.length > 0) { - const [selectedDashboard] = dashboards || []; - const { - data: { variables = {} }, - } = selectedDashboard; - - const { start, end } = getStartEndRangeTime({ - type: 'GLOBAL_TIME', - interval: globalTime.selectedTime, - }); - - const variablesTuple: Record = { - SIGNOZ_START_TIME: parseInt(start, 10) * 1e3, - SIGNOZ_END_TIME: parseInt(end, 10) * 1e3, - }; - Object.keys(variables).forEach((key) => { - variablesTuple[key] = variables[key].selectedValue; - }); - return variablesTuple; - } +export const getDashboardVariables = ( + variables?: Dashboard['data']['variables'], +): Record => { + if (!variables) { return {}; + } + + try { + const { globalTime } = store.getState(); + const { start, end } = getStartEndRangeTime({ + type: 'GLOBAL_TIME', + interval: globalTime.selectedTime, + }); + + const variablesTuple: Record = { + SIGNOZ_START_TIME: parseInt(start, 10) * 1e3, + SIGNOZ_END_TIME: parseInt(end, 10) * 1e3, + }; + Object.keys(variables).forEach((key) => { + variablesTuple[key] = variables[key].selectedValue; + }); + return variablesTuple; } catch (e) { console.error(e); } diff --git a/frontend/src/store/actions/dashboard/getQueryResults.ts b/frontend/src/lib/dashboard/getQueryResults.ts similarity index 99% rename from frontend/src/store/actions/dashboard/getQueryResults.ts rename to frontend/src/lib/dashboard/getQueryResults.ts index 2e6beae483..47f21bdd25 100644 --- a/frontend/src/store/actions/dashboard/getQueryResults.ts +++ b/frontend/src/lib/dashboard/getQueryResults.ts @@ -3,15 +3,16 @@ // @ts-nocheck import { getMetricsQueryRange } from 'api/metrics/getQueryRange'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Time } from 'container/TopNav/DateTimeSelection/config'; +import { Pagination } from 'hooks/queryPagination'; import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld'; import { isEmpty } from 'lodash-es'; import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; -import { Pagination } from 'hooks/queryPagination'; -import { PANEL_TYPES } from 'constants/queryBuilder'; + import { prepareQueryRangePayload } from './prepareQueryRangePayload'; export async function GetMetricQueryRange( diff --git a/frontend/src/store/actions/dashboard/prepareQueryRangePayload.ts b/frontend/src/lib/dashboard/prepareQueryRangePayload.ts similarity index 90% rename from frontend/src/store/actions/dashboard/prepareQueryRangePayload.ts rename to frontend/src/lib/dashboard/prepareQueryRangePayload.ts index f8a41dcd97..0b918bcb14 100644 --- a/frontend/src/store/actions/dashboard/prepareQueryRangePayload.ts +++ b/frontend/src/lib/dashboard/prepareQueryRangePayload.ts @@ -22,6 +22,7 @@ export const prepareQueryRangePayload = ({ params = {}, }: GetQueryResultsProps): PrepareQueryRangePayload => { let legendMap: Record = {}; + const { allowSelectedIntervalForStepGen, ...restParams } = params; const compositeQuery: QueryRangePayload['compositeQuery'] = { queryType: query.queryType, @@ -90,13 +91,17 @@ export const prepareQueryRangePayload = ({ start: parseInt(start, 10) * 1e3, end: parseInt(end, 10) * 1e3, step: getStep({ - start: store.getState().globalTime.minTime, - end: store.getState().globalTime.maxTime, + start: allowSelectedIntervalForStepGen + ? start + : store.getState().globalTime.minTime, + end: allowSelectedIntervalForStepGen + ? end + : store.getState().globalTime.maxTime, inputFormat: 'ns', }), variables, compositeQuery, - ...params, + ...restParams, }; return { legendMap, queryPayload }; diff --git a/frontend/src/lib/getChartData.ts b/frontend/src/lib/getChartData.ts index b45ee3d9fb..494dd76583 100644 --- a/frontend/src/lib/getChartData.ts +++ b/frontend/src/lib/getChartData.ts @@ -5,10 +5,17 @@ import { QueryData } from 'types/api/widgets/getQuery'; import convertIntoEpoc from './covertIntoEpoc'; import { colors } from './getRandomColor'; +export const limit = 30; + const getChartData = ({ queryData, createDataset, -}: GetChartDataProps): ChartData => { + isWarningLimit = false, +}: GetChartDataProps): { + data: ChartData; + isWarning: boolean; + // eslint-disable-next-line sonarjs/cognitive-complexity +} => { const uniqueTimeLabels = new Set(); queryData.forEach((data) => { data.queryData.forEach((query) => { @@ -17,6 +24,7 @@ const getChartData = ({ }); }); }); + const labels = Array.from(uniqueTimeLabels).sort((a, b) => a - b); const response = queryData.map( @@ -49,40 +57,66 @@ const getChartData = ({ return { label: labelNames !== 'undefined' ? labelNames : '', - first: filledDataValues.map((e) => e.first), - second: filledDataValues.map((e) => e.second), + first: filledDataValues.map((e) => e.first || 0), + second: filledDataValues.map((e) => e.second || 0), }; }), ); + const allLabels = response .map((e) => e.map((e) => e.label)) .reduce((a, b) => [...a, ...b], []); - const alldata = response - .map((e) => e.map((e) => e.second)) - .reduce((a, b) => [...a, ...b], []); + const modifiedData = response + .flat() + .sort((a, b) => { + const len = Math.min(a.second.length, b.second.length); // min length of both array + + for (let i = 0; i < len; i += 1) { + const avearageOfArray = (arr: number[]): number => + arr.reduce((a, b) => a + b, 0) / arr.length; + + const diff = avearageOfArray(a.second) - avearageOfArray(b.second); // calculating the difference + + if (diff !== 0) return diff; + } + + return a.second.length - b.second.length; + }) + .reverse(); + + const updatedSortedData = isWarningLimit + ? modifiedData.slice(0, limit) + : modifiedData; + + const updatedDataSet = updatedSortedData.map((e, index) => { + const datasetBaseConfig = { + index, + label: allLabels[index], + borderColor: colors[index % colors.length] || 'red', + data: e.second, + borderWidth: 1.5, + spanGaps: true, + animations: false, + showLine: true, + pointRadius: 0, + }; + + return createDataset + ? createDataset(e.second, index, allLabels) + : datasetBaseConfig; + }); + + const updatedLabels = modifiedData.map((e) => e.first).flat(); + + const updatedData = { + datasets: updatedDataSet, + labels: updatedLabels, + }; return { - datasets: alldata.map((e, index) => { - const datasetBaseConfig = { - index, - label: allLabels[index], - borderColor: colors[index % colors.length] || 'red', - data: e, - borderWidth: 1.5, - spanGaps: true, - animations: false, - showLine: true, - pointRadius: 0, - }; - - return createDataset - ? createDataset(e, index, allLabels) - : datasetBaseConfig; - }), - labels: response - .map((e) => e.map((e) => e.first)) - .reduce((a, b) => [...a, ...b], [])[0], + data: updatedData, + isWarning: isWarningLimit && (allLabels?.length || 0) > limit, }; }; @@ -97,6 +131,7 @@ export interface GetChartDataProps { index: number, allLabels: string[], ) => ChartDataset; + isWarningLimit?: boolean; } export default getChartData; diff --git a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi.ts b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi.ts index f89c8b025e..1dd30b249b 100644 --- a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi.ts +++ b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi.ts @@ -1,4 +1,4 @@ -import { GetQueryResultsProps } from 'store/actions/dashboard/getQueryResults'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import { MapData, MapQueryDataToApiResult, diff --git a/frontend/src/pages/DashboardWidget/index.tsx b/frontend/src/pages/DashboardWidget/index.tsx index fc8a712b63..9dd439d86c 100644 --- a/frontend/src/pages/DashboardWidget/index.tsx +++ b/frontend/src/pages/DashboardWidget/index.tsx @@ -1,30 +1,24 @@ import { Card, Typography } from 'antd'; import Spinner from 'components/Spinner'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import NewWidget from 'container/NewWidget'; +import useUrlQuery from 'hooks/useUrlQuery'; import history from 'lib/history'; -import { useEffect, useRef, useState } from 'react'; -import { connect, useSelector } from 'react-redux'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useEffect, useState } from 'react'; import { generatePath, useLocation, useParams } from 'react-router-dom'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { GetDashboard, GetDashboardProps } from 'store/actions'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import DashboardReducer from 'types/reducer/dashboards'; -function DashboardWidget({ getDashboard }: NewDashboardProps): JSX.Element { +function DashboardWidget(): JSX.Element | null { const { search } = useLocation(); const { dashboardId } = useParams(); const [selectedGraph, setSelectedGraph] = useState(); - const { loading, dashboards, error, errorMessage } = useSelector< - AppState, - DashboardReducer - >((state) => state.dashboards); - const [selectedDashboard] = dashboards; - const params = new URLSearchParams(search); + + const { selectedDashboard, dashboardResponse } = useDashboard(); + + const params = useUrlQuery(); const widgetId = params.get('widgetId'); const { data } = selectedDashboard || {}; @@ -43,37 +37,22 @@ function DashboardWidget({ getDashboard }: NewDashboardProps): JSX.Element { } }, [dashboardId, search]); - const counter = useRef(0); - - useEffect(() => { - if (counter.current === 0 && selectedGraph && widgetId !== null) { - counter.current = 1; - getDashboard({ - uuid: dashboardId, - graphType: selectedGraph, - widgetId, - }); - } - }, [selectedGraph, dashboardId, getDashboard, search, widgetId]); - - if ( - selectedGraph === undefined || - loading || - dashboards.length === 0 || - dashboards[0].data.widgets === undefined || - selectedWidget === undefined - ) { + if (selectedGraph === undefined || dashboardResponse.isLoading) { return ; } - if (error) { + if (dashboardResponse.isError) { return ( - {errorMessage} + {SOMETHING_WENT_WRONG} ); } + if (selectedWidget === undefined) { + return null; + } + return ( (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getDashboard: bindActionCreators(GetDashboard, dispatch), -}); - -type NewDashboardProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(DashboardWidget); +export default DashboardWidget; diff --git a/frontend/src/pages/NewDashboard/index.tsx b/frontend/src/pages/NewDashboard/index.tsx index 98ed3d394f..105d21cfdf 100644 --- a/frontend/src/pages/NewDashboard/index.tsx +++ b/frontend/src/pages/NewDashboard/index.tsx @@ -1,70 +1,33 @@ import { Typography } from 'antd'; +import { AxiosError } from 'axios'; import NotFound from 'components/NotFound'; import Spinner from 'components/Spinner'; import NewDashboard from 'container/NewDashboard'; -import { useEffect } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { GetDashboard, GetDashboardProps } from 'store/actions/dashboard'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { ErrorType } from 'types/common'; -import DashboardReducer from 'types/reducer/dashboards'; -function NewDashboardPage({ getDashboard }: NewDashboardProps): JSX.Element { - const { loading, dashboards, error, errorMessage } = useSelector< - AppState, - DashboardReducer - >((state) => state.dashboards); +function NewDashboardPage(): JSX.Element { + const { dashboardResponse } = useDashboard(); - const { dashboardId } = useParams(); + const { isFetching, isError, isLoading } = dashboardResponse; - useEffect(() => { - getDashboard({ - uuid: dashboardId, - }); - }, [getDashboard, dashboardId]); + const errorMessage = isError + ? (dashboardResponse?.error as AxiosError)?.response?.data.errorType + : 'Something went wrong'; - if ( - error && - !loading && - dashboards.length === 0 && - errorMessage === ErrorType.NotFound - ) { + if (isError && !isFetching && errorMessage === ErrorType.NotFound) { return ; } - if (error && !loading && dashboards.length === 0) { + if (isError && errorMessage) { return {errorMessage}; } - // when user comes from dashboard page. dashboard array is populated with some dashboard as dashboard is populated - // so to avoid any unmount call dashboard must have length zero - if (loading || dashboards.length === 0 || dashboards.length !== 1) { + if (isLoading) { return ; } return ; } -interface Params { - dashboardId: string; -} - -interface DispatchProps { - getDashboard: ( - props: GetDashboardProps, - ) => (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getDashboard: bindActionCreators(GetDashboard, dispatch), -}); - -type NewDashboardProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(NewDashboardPage); +export default NewDashboardPage; diff --git a/frontend/src/pages/Settings/config.ts b/frontend/src/pages/Settings/config.ts index ceb06b7bb5..a24a3a5097 100644 --- a/frontend/src/pages/Settings/config.ts +++ b/frontend/src/pages/Settings/config.ts @@ -2,6 +2,7 @@ import { RouteTabProps } from 'components/RouteTab/types'; import ROUTES from 'constants/routes'; import AlertChannels from 'container/AllAlertChannels'; import GeneralSettings from 'container/GeneralSettings'; +import IngestionSettings from 'container/IngestionSettings/IngestionSettings'; import OrganizationSettings from 'container/OrganizationSettings'; import { TFunction } from 'i18next'; @@ -18,6 +19,12 @@ export const commonRoutes = (t: TFunction): RouteTabProps['routes'] => [ route: ROUTES.ALL_CHANNELS, key: ROUTES.ALL_CHANNELS, }, + { + Component: IngestionSettings, + name: t('routes:ingestion_settings').toString(), + route: ROUTES.INGESTION_SETTINGS, + key: ROUTES.INGESTION_SETTINGS, + }, ]; export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [ diff --git a/frontend/src/pages/SignUp/SignUp.tsx b/frontend/src/pages/SignUp/SignUp.tsx index c4f90d0e20..910ff91b63 100644 --- a/frontend/src/pages/SignUp/SignUp.tsx +++ b/frontend/src/pages/SignUp/SignUp.tsx @@ -233,6 +233,8 @@ function SignUp({ version }: SignUpProps): JSX.Element { const handleSubmit = (): void => { (async (): Promise => { + const { hostname } = window.location; + try { const values = form.getFieldsValue(); setLoading(true); @@ -258,7 +260,11 @@ function SignUp({ version }: SignUpProps): JSX.Element { await commonHandler( values, async (): Promise => { - if (isOnboardingEnabled) { + if ( + isOnboardingEnabled && + hostname && + hostname.endsWith('signoz.cloud') + ) { history.push(ROUTES.GET_STARTED); } else { history.push(ROUTES.APPLICATION); diff --git a/frontend/src/providers/Dashboard/Dashboard.tsx b/frontend/src/providers/Dashboard/Dashboard.tsx new file mode 100644 index 0000000000..9b4da727ae --- /dev/null +++ b/frontend/src/providers/Dashboard/Dashboard.tsx @@ -0,0 +1,231 @@ +import { Modal } from 'antd'; +import get from 'api/dashboard/get'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import ROUTES from 'constants/routes'; +import { getMinMax } from 'container/TopNav/AutoRefresh/config'; +import dayjs, { Dayjs } from 'dayjs'; +import useTabVisibility from 'hooks/useTabFocus'; +import { + createContext, + PropsWithChildren, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Layout } from 'react-grid-layout'; +import { useTranslation } from 'react-i18next'; +import { useQuery, UseQueryResult } from 'react-query'; +import { useDispatch, useSelector } from 'react-redux'; +import { useRouteMatch } from 'react-router-dom'; +import { Dispatch } from 'redux'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; +import { Dashboard } from 'types/api/dashboard/getAll'; +import AppReducer from 'types/reducer/app'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { IDashboardContext } from './types'; + +const DashboardContext = createContext({ + isDashboardSliderOpen: false, + handleToggleDashboardSlider: () => {}, + dashboardResponse: {} as UseQueryResult, + selectedDashboard: {} as Dashboard, + dashboardId: '', + layouts: [], + setLayouts: () => {}, + setSelectedDashboard: () => {}, + updatedTimeRef: {} as React.MutableRefObject, +}); + +interface Props { + dashboardId: string; +} + +export function DashboardProvider({ + children, +}: PropsWithChildren): JSX.Element { + const [isDashboardSliderOpen, setIsDashboardSlider] = useState(false); + const isDashboardPage = useRouteMatch({ + path: ROUTES.DASHBOARD, + exact: true, + }); + + const dispatch = useDispatch>(); + + const globalTime = useSelector( + (state) => state.globalTime, + ); + + const [onModal, Content] = Modal.useModal(); + + const isDashboardWidgetPage = useRouteMatch({ + path: ROUTES.DASHBOARD_WIDGET, + exact: true, + }); + + const [layouts, setLayouts] = useState([]); + + const { isLoggedIn } = useSelector((state) => state.app); + + const dashboardId = + (isDashboardPage + ? isDashboardPage.params.dashboardId + : isDashboardWidgetPage?.params.dashboardId) || ''; + + const [selectedDashboard, setSelectedDashboard] = useState(); + + const updatedTimeRef = useRef(null); // Using ref to store the updated time + const modalRef = useRef(null); + + const isVisible = useTabVisibility(); + + const { t } = useTranslation(['dashboard']); + const dashboardRef = useRef(); + + const dashboardResponse = useQuery( + [REACT_QUERY_KEY.DASHBOARD_BY_ID, isDashboardPage?.params], + { + enabled: (!!isDashboardPage || !!isDashboardWidgetPage) && isLoggedIn, + queryFn: () => + get({ + uuid: dashboardId, + }), + refetchOnWindowFocus: false, + onSuccess: (data) => { + const updatedDate = dayjs(data.updated_at); + + // on first render + if (updatedTimeRef.current === null) { + setSelectedDashboard(data); + + updatedTimeRef.current = updatedDate; + + dashboardRef.current = data; + + setLayouts( + data.data.layout?.filter( + (layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET, + ) || [], + ); + } + + if ( + updatedTimeRef.current !== null && + updatedDate.isAfter(updatedTimeRef.current) && + isVisible && + dashboardRef.current?.id === data.id + ) { + // show modal when state is out of sync + const modal = onModal.confirm({ + centered: true, + title: t('dashboard_has_been_updated'), + content: t('do_you_want_to_refresh_the_dashboard'), + onOk() { + setSelectedDashboard(data); + + const { maxTime, minTime } = getMinMax( + globalTime.selectedTime, + globalTime.minTime, + globalTime.maxTime, + ); + + dispatch({ + type: UPDATE_TIME_INTERVAL, + payload: { + maxTime, + minTime, + selectedTime: globalTime.selectedTime, + }, + }); + + dashboardRef.current = data; + + updatedTimeRef.current = dayjs(data.updated_at); + + setLayouts( + data.data.layout?.filter( + (layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET, + ) || [], + ); + }, + }); + + modalRef.current = modal; + } else { + // normal flow + updatedTimeRef.current = dayjs(data.updated_at); + + dashboardRef.current = data; + + setSelectedDashboard(data); + + setLayouts( + data.data.layout?.filter( + (layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET, + ) || [], + ); + } + }, + }, + ); + + useEffect(() => { + if (isVisible && updatedTimeRef.current) { + dashboardResponse.refetch(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible]); + + useEffect(() => { + if (!isVisible && modalRef.current) { + modalRef.current.destroy(); + } + }, [isVisible]); + + const handleToggleDashboardSlider = (value: boolean): void => { + setIsDashboardSlider(value); + }; + + const value: IDashboardContext = useMemo( + () => ({ + isDashboardSliderOpen, + handleToggleDashboardSlider, + dashboardResponse, + selectedDashboard, + dashboardId, + layouts, + setLayouts, + setSelectedDashboard, + updatedTimeRef, + }), + [ + isDashboardSliderOpen, + dashboardResponse, + selectedDashboard, + dashboardId, + layouts, + ], + ); + + return ( + + {Content} + {children} + + ); +} + +export const useDashboard = (): IDashboardContext => { + const context = useContext(DashboardContext); + + if (!context) { + throw new Error('Should be used inside the context'); + } + + return context; +}; diff --git a/frontend/src/providers/Dashboard/types.ts b/frontend/src/providers/Dashboard/types.ts new file mode 100644 index 0000000000..9fc15c5f77 --- /dev/null +++ b/frontend/src/providers/Dashboard/types.ts @@ -0,0 +1,18 @@ +import dayjs from 'dayjs'; +import { Layout } from 'react-grid-layout'; +import { UseQueryResult } from 'react-query'; +import { Dashboard } from 'types/api/dashboard/getAll'; + +export interface IDashboardContext { + isDashboardSliderOpen: boolean; + handleToggleDashboardSlider: (value: boolean) => void; + dashboardResponse: UseQueryResult; + selectedDashboard: Dashboard | undefined; + dashboardId: string; + layouts: Layout[]; + setLayouts: React.Dispatch>; + setSelectedDashboard: React.Dispatch< + React.SetStateAction + >; + updatedTimeRef: React.MutableRefObject; +} diff --git a/frontend/src/providers/Dashboard/util.ts b/frontend/src/providers/Dashboard/util.ts new file mode 100644 index 0000000000..40ec15f75e --- /dev/null +++ b/frontend/src/providers/Dashboard/util.ts @@ -0,0 +1,22 @@ +import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; + +export const getPreviousWidgets = ( + selectedDashboard: Dashboard, + selectedWidgetIndex: number, +): Widgets[] => + selectedDashboard.data.widgets?.slice(0, selectedWidgetIndex || 0) || []; + +export const getNextWidgets = ( + selectedDashboard: Dashboard, + selectedWidgetIndex: number, +): Widgets[] => + selectedDashboard.data.widgets?.slice( + (selectedWidgetIndex || 0) + 1, // this is never undefined + selectedDashboard.data.widgets?.length, + ) || []; + +export const getSelectedWidgetIndex = ( + selectedDashboard: Dashboard, + widgetId: string | null, +): number => + selectedDashboard.data.widgets?.findIndex((e) => e.id === widgetId) || 0; diff --git a/frontend/src/store/actions/dashboard/applySettingsToPanel.ts b/frontend/src/store/actions/dashboard/applySettingsToPanel.ts deleted file mode 100644 index 0a9a6bdd5f..0000000000 --- a/frontend/src/store/actions/dashboard/applySettingsToPanel.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { Widgets } from 'types/api/dashboard/getAll'; - -export const ApplySettingsToPanel = ( - props: ApplySettingsToPanelProps, -): ((dispatch: Dispatch) => void) => ( - dispatch: Dispatch, -): void => { - dispatch({ - type: 'APPLY_SETTINGS_TO_PANEL', - payload: props, - }); -}; - -export interface ApplySettingsToPanelProps { - title: Widgets['title']; - description: Widgets['description']; - opacity: Widgets['opacity']; - isStacked: Widgets['isStacked']; - timePreferance: Widgets['timePreferance']; - nullZeroValues: Widgets['nullZeroValues']; - widgetId: Widgets['id']; - yAxisUnit: Widgets['yAxisUnit']; -} diff --git a/frontend/src/store/actions/dashboard/deleteDashboard.ts b/frontend/src/store/actions/dashboard/deleteDashboard.ts deleted file mode 100644 index a8add8002a..0000000000 --- a/frontend/src/store/actions/dashboard/deleteDashboard.ts +++ /dev/null @@ -1,49 +0,0 @@ -import deleteDashboardApi from 'api/dashboard/delete'; -import { UseQueryResult } from 'react-query'; -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { Dashboard } from 'types/api/dashboard/getAll'; - -export const DeleteDashboard = ({ - uuid, - refetch, -}: DeleteDashboardProps): ((dispatch: Dispatch) => void) => async ( - dispatch: Dispatch, -): Promise => { - try { - const response = await deleteDashboardApi({ - uuid, - }); - - if (response.statusCode === 200) { - refetch(); - - dispatch({ - type: 'DELETE_DASHBOARD_SUCCESS', - payload: { - uuid, - }, - }); - } else { - dispatch({ - type: 'DELETE_DASHBOARD_ERROR', - payload: { - errorMessage: response.error || 'Something went wrong', - }, - }); - } - } catch (error) { - dispatch({ - type: 'DELETE_DASHBOARD_ERROR', - payload: { - errorMessage: - error instanceof Error ? error.toString() : 'Something went wrong', - }, - }); - } -}; - -export interface DeleteDashboardProps { - uuid: Dashboard['uuid']; - refetch: UseQueryResult['refetch']; -} diff --git a/frontend/src/store/actions/dashboard/deleteQuery.ts b/frontend/src/store/actions/dashboard/deleteQuery.ts deleted file mode 100644 index 0b0131d05f..0000000000 --- a/frontend/src/store/actions/dashboard/deleteQuery.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { DeleteQueryProps } from 'types/actions/dashboard'; - -export const DeleteQuery = ( - props: DeleteQueryProps, -): ((dispatch: Dispatch) => void) => ( - dispatch: Dispatch, -): void => { - dispatch({ - type: 'DELETE_QUERY', - payload: { - currentIndex: props.currentIndex, - widgetId: props.widgetId, - }, - }); -}; diff --git a/frontend/src/store/actions/dashboard/deleteWidget.ts b/frontend/src/store/actions/dashboard/deleteWidget.ts deleted file mode 100644 index d4a29db3f5..0000000000 --- a/frontend/src/store/actions/dashboard/deleteWidget.ts +++ /dev/null @@ -1,70 +0,0 @@ -import updateDashboardApi from 'api/dashboard/update'; -import { AxiosError } from 'axios'; -import { getPreLayouts, LayoutProps } from 'container/GridGraphLayout'; -import { Dispatch, SetStateAction } from 'react'; -import store from 'store'; -import AppActions from 'types/actions'; -import { UPDATE_DASHBOARD } from 'types/actions/dashboard'; -import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; - -export const DeleteWidget = ({ - widgetId, - setLayout, -}: DeleteWidgetProps): ((dispatch: Dispatch) => void) => async ( - dispatch: Dispatch, -): Promise => { - try { - const { dashboards } = store.getState(); - const [selectedDashboard] = dashboards.dashboards; - - const { widgets = [] } = selectedDashboard.data; - const updatedWidgets = widgets.filter((e) => e.id !== widgetId); - const updatedLayout = - selectedDashboard.data.layout?.filter((e) => e.i !== widgetId) || []; - - const updatedSelectedDashboard: Dashboard = { - ...selectedDashboard, - data: { - title: selectedDashboard.data.title, - description: selectedDashboard.data.description, - name: selectedDashboard.data.name, - tags: selectedDashboard.data.tags, - widgets: updatedWidgets, - layout: updatedLayout, - variables: selectedDashboard.data.variables, - }, - uuid: selectedDashboard.uuid, - }; - - const response = await updateDashboardApi(updatedSelectedDashboard); - - if (response.statusCode === 200) { - dispatch({ - type: UPDATE_DASHBOARD, - payload: updatedSelectedDashboard, - }); - if (setLayout) { - setLayout(getPreLayouts(updatedWidgets, updatedLayout)); - } - } else { - dispatch({ - type: 'DELETE_WIDGET_ERROR', - payload: { - errorMessage: response.error || 'Something went wrong', - }, - }); - } - } catch (error) { - dispatch({ - type: 'DELETE_WIDGET_ERROR', - payload: { - errorMessage: (error as AxiosError).toString() || 'Something went wrong', - }, - }); - } -}; - -export interface DeleteWidgetProps { - widgetId: Widgets['id']; - setLayout?: Dispatch>; -} diff --git a/frontend/src/store/actions/dashboard/getDashboard.ts b/frontend/src/store/actions/dashboard/getDashboard.ts deleted file mode 100644 index 76305bae3b..0000000000 --- a/frontend/src/store/actions/dashboard/getDashboard.ts +++ /dev/null @@ -1,68 +0,0 @@ -import getDashboard from 'api/dashboard/get'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { Props } from 'types/api/dashboard/get'; - -export const GetDashboard = ({ - uuid, - widgetId, - graphType, -}: GetDashboardProps): ((dispatch: Dispatch) => void) => async ( - dispatch: Dispatch, -): Promise => { - try { - dispatch({ - type: 'GET_DASHBOARD_LOADING_START', - }); - - const response = await getDashboard({ - uuid, - }); - - if (response.statusCode === 200) { - dispatch({ - payload: response.payload, - type: 'GET_DASHBOARD_SUCCESS', - }); - - if (widgetId !== undefined) { - dispatch({ - type: 'CREATE_DEFAULT_WIDGET', - payload: { - description: '', - id: widgetId, - isStacked: false, - nullZeroValues: 'zero', - opacity: '0', - panelTypes: graphType || PANEL_TYPES.TIME_SERIES, - timePreferance: 'GLOBAL_TIME', - title: '', - query: initialQueriesMap.metrics, - }, - }); - } - } else { - dispatch({ - type: 'GET_DASHBOARD_ERROR', - payload: { - errorMessage: response.error || 'Something went wrong', - }, - }); - } - } catch (error) { - dispatch({ - type: 'GET_DASHBOARD_ERROR', - payload: { - errorMessage: - error instanceof Error ? error.toString() : 'Something went wrong', - }, - }); - } -}; - -export interface GetDashboardProps { - uuid: Props['uuid']; - widgetId?: string; - graphType?: PANEL_TYPES; -} diff --git a/frontend/src/store/actions/dashboard/index.ts b/frontend/src/store/actions/dashboard/index.ts deleted file mode 100644 index b01947fe3d..0000000000 --- a/frontend/src/store/actions/dashboard/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './applySettingsToPanel'; -export * from './deleteDashboard'; -export * from './deleteQuery'; -export * from './getDashboard'; -export * from './toggleEditMode'; -export * from './updateDashboardTitle'; diff --git a/frontend/src/store/actions/dashboard/saveDashboard.ts b/frontend/src/store/actions/dashboard/saveDashboard.ts deleted file mode 100644 index 614c66b18c..0000000000 --- a/frontend/src/store/actions/dashboard/saveDashboard.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { notification } from 'antd'; -import updateDashboardApi from 'api/dashboard/update'; -import { AxiosError } from 'axios'; -import { QueryParams } from 'constants/query'; -import { PANEL_TYPES } from 'constants/queryBuilder'; -import ROUTES from 'constants/routes'; -import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; -import history from 'lib/history'; -import { Layout } from 'react-grid-layout'; -import { generatePath } from 'react-router-dom'; -import { Dispatch } from 'redux'; -import store from 'store'; -import AppActions from 'types/actions'; -import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; -import { v4 } from 'uuid'; - -export const SaveDashboard = ({ - uuid, - description, - isStacked, - nullZeroValues, - opacity, - timePreferance, - title, - widgetId, - dashboardId, - yAxisUnit, - graphType, -}: SaveDashboardProps): ((dispatch: Dispatch) => void) => - // eslint-disable-next-line sonarjs/cognitive-complexity - async (dispatch: Dispatch): Promise => { - try { - const dashboard = store.getState(); - - const selectedDashboard = dashboard.dashboards.dashboards.find( - (e) => e.uuid === uuid, - ); - - if (selectedDashboard === undefined) { - throw new Error('Dashboard Not Found'); - } - - const { data } = selectedDashboard; - - const updatedTitle = title; - const updatedDescription = description; - const updatedisStacked = isStacked; - const updatednullZeroValues = nullZeroValues; - const updatedopacity = opacity; - const updatedtimePreferance = timePreferance; - const updatedYAxisUnit = yAxisUnit; - - const selectedWidgetIndex = data.widgets?.findIndex( - (e) => e.id === widgetId, - ); - - const isEmptyWidget = widgetId === 'empty'; - - const emptyLayoutIndex = data.layout?.findIndex((e) => e.i === 'empty'); - - const newWidgetId = v4(); - - const preWidget = data.widgets?.slice(0, selectedWidgetIndex) || []; - - const afterWidget = - data.widgets?.slice( - (selectedWidgetIndex || 0) + 1, // this is never undefined - data.widgets?.length, - ) || []; - - const selectedWidget = (selectedDashboard.data.widgets || [])[ - selectedWidgetIndex || 0 - ]; - - const getAllLayout = (): Layout[] => { - const allLayout = data.layout || []; - - // empty layout is not present - if (emptyLayoutIndex === -1 || emptyLayoutIndex === undefined) { - return allLayout; - } - - return [ - ...allLayout.slice(0, emptyLayoutIndex), - { ...allLayout[emptyLayoutIndex], i: newWidgetId }, - ...allLayout.slice(emptyLayoutIndex + 1, allLayout.length), - ]; - }; - const allLayout = getAllLayout(); - const params = new URLSearchParams(window.location.search); - const compositeQuery = params.get(QueryParams.compositeQuery); - const { maxTime, minTime } = store.getState().globalTime; - const query = compositeQuery - ? updateStepInterval( - JSON.parse(decodeURIComponent(compositeQuery)), - maxTime, - minTime, - ) - : updateStepInterval(selectedWidget.query, maxTime, minTime); - - const response = await updateDashboardApi({ - data: { - ...selectedDashboard.data, - // this is the data for the dashboard - title: selectedDashboard.data.title, - description: selectedDashboard.data.description, - tags: selectedDashboard.data.tags, - name: selectedDashboard.data.name, - layout: allLayout, - // as we are updated the widget only - widgets: [ - ...preWidget, - { - ...selectedWidget, - query, - description: updatedDescription, - id: isEmptyWidget ? newWidgetId : widgetId, - isStacked: updatedisStacked, - nullZeroValues: updatednullZeroValues, - opacity: updatedopacity, - title: updatedTitle, - timePreferance: updatedtimePreferance, - yAxisUnit: updatedYAxisUnit, - panelTypes: graphType, - }, - ...afterWidget, - ], - }, - uuid, - }); - - if (response.statusCode === 200) { - dispatch({ - type: 'SAVE_SETTING_TO_PANEL_SUCCESS', - payload: response.payload, - }); - 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 || error, - }, - }); - } - } catch (error) { - dispatch({ - type: 'SAVE_SETTING_TO_PANEL_ERROR', - payload: { - errorMessage: (error as AxiosError).toString() || 'Something went wrong', - }, - }); - } - }; - -export interface SaveDashboardProps { - uuid: Dashboard['uuid']; - title: Widgets['title']; - description: Widgets['description']; - opacity: Widgets['opacity']; - isStacked: Widgets['isStacked']; - timePreferance: Widgets['timePreferance']; - nullZeroValues: Widgets['nullZeroValues']; - widgetId: Widgets['id']; - dashboardId: string; - yAxisUnit: Widgets['yAxisUnit']; - graphType: PANEL_TYPES; -} diff --git a/frontend/src/store/actions/dashboard/toggleAddWidget.ts b/frontend/src/store/actions/dashboard/toggleAddWidget.ts deleted file mode 100644 index ba2ad08107..0000000000 --- a/frontend/src/store/actions/dashboard/toggleAddWidget.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; - -export const ToggleAddWidget = ( - props: ToggleAddWidgetProps, -): ((dispatch: Dispatch) => void) => ( - dispatch: Dispatch, -): void => { - dispatch({ - type: 'IS_ADD_WIDGET', - payload: { - isAddWidget: props, - }, - }); -}; - -export type ToggleAddWidgetProps = boolean; diff --git a/frontend/src/store/actions/dashboard/toggleEditMode.ts b/frontend/src/store/actions/dashboard/toggleEditMode.ts deleted file mode 100644 index f03d6c5ff4..0000000000 --- a/frontend/src/store/actions/dashboard/toggleEditMode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; - -export const ToggleEditMode = (): (( - dispatch: Dispatch, -) => void) => (dispatch: Dispatch): void => { - dispatch({ - type: 'TOGGLE_EDIT_MODE', - }); -}; diff --git a/frontend/src/store/actions/dashboard/updateDashboardTitle.ts b/frontend/src/store/actions/dashboard/updateDashboardTitle.ts deleted file mode 100644 index 4400e3ea39..0000000000 --- a/frontend/src/store/actions/dashboard/updateDashboardTitle.ts +++ /dev/null @@ -1,55 +0,0 @@ -import update from 'api/dashboard/update'; -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { Dashboard } from 'types/api/dashboard/getAll'; - -export const UpdateDashboardTitleDescriptionTags = ({ - dashboard, // @TODO need to grab the dashboard from the store -}: UpdateDashboardTitleDescriptionTagsProps): (( - dispatch: Dispatch, -) => void) => async (dispatch: Dispatch): Promise => { - try { - const { data } = dashboard; - - const response = await update({ - data: { - ...dashboard.data, - title: dashboard.data.title, - }, - uuid: dashboard.uuid, - }); - - if (response.statusCode === 200) { - dispatch({ - type: 'UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS', - payload: { - description: data.description, - tags: data.tags, - title: dashboard.data.title, - }, - }); - dispatch({ - type: 'TOGGLE_EDIT_MODE', - }); - } else { - dispatch({ - type: 'UPDATE_TITLE_DESCRIPTION_TAGS_ERROR', - payload: { - errorMessage: response.error || 'Something went wrong', - }, - }); - } - } catch (error) { - dispatch({ - type: 'UPDATE_TITLE_DESCRIPTION_TAGS_ERROR', - payload: { - errorMessage: - error instanceof Error ? error.toString() : 'Something went wrong', - }, - }); - } -}; - -export interface UpdateDashboardTitleDescriptionTagsProps { - dashboard: Dashboard; -} diff --git a/frontend/src/store/actions/dashboard/updateQuery.ts b/frontend/src/store/actions/dashboard/updateQuery.ts deleted file mode 100644 index 5b30c4dfe3..0000000000 --- a/frontend/src/store/actions/dashboard/updateQuery.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; - -export const UpdateQuery = ( - props: UpdateQueryProps, -): ((dispatch: Dispatch) => void) => ( - dispatch: Dispatch, -): void => { - dispatch({ - type: 'UPDATE_QUERY', - payload: { - widgetId: props.widgetId, - yAxisUnit: props.yAxisUnit, - }, - }); -}; - -export interface UpdateQueryProps { - widgetId: string; - yAxisUnit: string | undefined; -} diff --git a/frontend/src/store/actions/dashboard/updatedDashboardVariables.ts b/frontend/src/store/actions/dashboard/updatedDashboardVariables.ts deleted file mode 100644 index 4b1d2091f5..0000000000 --- a/frontend/src/store/actions/dashboard/updatedDashboardVariables.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { NotificationInstance } from 'antd/es/notification/interface'; -import update from 'api/dashboard/update'; -import { Dispatch } from 'redux'; -import store from 'store/index'; -import AppActions from 'types/actions'; -import { UPDATE_DASHBOARD_VARIABLES } from 'types/actions/dashboard'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -export const UpdateDashboardVariables = ( - variables: Record, - notify: NotificationInstance, -): ((dispatch: Dispatch) => void) => async ( - dispatch: Dispatch, -): Promise => { - try { - dispatch({ - type: UPDATE_DASHBOARD_VARIABLES, - payload: variables, - }); - - const reduxStoreState = store.getState(); - const [dashboard] = reduxStoreState.dashboards.dashboards; - - const response = await update({ - data: { - ...dashboard.data, - }, - uuid: dashboard.uuid, - }); - - if (response.statusCode !== 200) { - notify.error({ - message: response.error, - }); - } - } catch (error) { - console.error(error); - } -}; diff --git a/frontend/src/store/actions/index.ts b/frontend/src/store/actions/index.ts index 7281693e13..3074cdb3a2 100644 --- a/frontend/src/store/actions/index.ts +++ b/frontend/src/store/actions/index.ts @@ -1,5 +1,4 @@ export * from './app'; -export * from './dashboard'; export * from './global'; export * from './metrics'; export * from './serviceMap'; diff --git a/frontend/src/store/reducers/dashboard.ts b/frontend/src/store/reducers/dashboard.ts deleted file mode 100644 index f9140830b3..0000000000 --- a/frontend/src/store/reducers/dashboard.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { - APPLY_SETTINGS_TO_PANEL, - CREATE_DEFAULT_WIDGET, - DashboardActions, - DELETE_DASHBOARD_SUCCESS, - // DELETE_QUERY, - DELETE_WIDGET_SUCCESS, - FLUSH_DASHBOARD, - GET_ALL_DASHBOARD_ERROR, - GET_ALL_DASHBOARD_LOADING_START, - GET_ALL_DASHBOARD_SUCCESS, - GET_DASHBOARD_ERROR, - GET_DASHBOARD_LOADING_START, - GET_DASHBOARD_SUCCESS, - IS_ADD_WIDGET, - SAVE_SETTING_TO_PANEL_SUCCESS, - TOGGLE_EDIT_MODE, - UPDATE_DASHBOARD, - UPDATE_DASHBOARD_VARIABLES, - UPDATE_QUERY, - UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS, -} from 'types/actions/dashboard'; -import InitialValueTypes from 'types/reducer/dashboards'; - -const InitialValue: InitialValueTypes = { - dashboards: [], - loading: false, - error: false, - errorMessage: '', - isEditMode: false, - isAddWidget: false, -}; - -const dashboard = ( - state = InitialValue, - action: DashboardActions, - // eslint-disable-next-line sonarjs/cognitive-complexity -): InitialValueTypes => { - switch (action.type) { - case GET_ALL_DASHBOARD_LOADING_START: - case GET_DASHBOARD_LOADING_START: { - return { - ...state, - loading: true, - }; - } - - case GET_ALL_DASHBOARD_SUCCESS: { - return { - ...state, - loading: false, - dashboards: action.payload, - }; - } - - case GET_DASHBOARD_SUCCESS: { - const dashboard = action.payload; - const { data } = dashboard; - return { - ...state, - loading: false, - dashboards: [ - { - ...dashboard, - data: { - ...data, - }, - }, - ], - }; - } - - case GET_ALL_DASHBOARD_ERROR: { - const { payload } = action; - - return { - ...state, - loading: false, - error: true, - errorMessage: payload.errorMessage, - }; - } - - case GET_DASHBOARD_ERROR: { - return { - ...state, - loading: false, - errorMessage: action.payload.errorMessage, - error: true, - }; - } - - case UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS: { - const [dashboard] = state.dashboards; - - const dashboardData = dashboard.data; - const { tags, title, description } = action.payload; - - return { - ...state, - dashboards: [ - { - created_at: dashboard.created_at, - id: dashboard.id, - updated_at: dashboard.updated_at, - uuid: dashboard.uuid, - data: { - ...dashboardData, - tags, - title, - description, - }, - }, - ], - }; - } - - case TOGGLE_EDIT_MODE: { - return { - ...state, - isEditMode: !state.isEditMode, - }; - } - - case DELETE_DASHBOARD_SUCCESS: { - return { - ...state, - dashboards: state.dashboards.filter((e) => e.uuid !== action.payload.uuid), - }; - } - - // NOTE: this action will will be dispatched in the single dashboard only - case CREATE_DEFAULT_WIDGET: { - const [selectedDashboard] = state.dashboards; - const { data } = selectedDashboard; - const { widgets } = data; - const defaultWidget = action.payload; - const { query } = action.payload; - - const isPresent = widgets?.find((e) => e.id === action.payload.id); - - if (isPresent !== undefined) { - return { - ...state, - }; - } - - return { - ...state, - dashboards: [ - { - ...selectedDashboard, - data: { - ...data, - widgets: [ - ...(widgets || []), - { - ...defaultWidget, - query, - id: action.payload.id, - }, - ], - }, - }, - ], - }; - } - - case APPLY_SETTINGS_TO_PANEL: { - const { widgetId } = action.payload; - - const { dashboards } = state; - const [selectedDashboard] = dashboards; - const { data } = selectedDashboard; - const { widgets } = data; - - const selectedWidgetIndex = data.widgets?.findIndex( - (e) => e.id === widgetId, - ); - - 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, - description: action.payload.description, - id: action.payload.widgetId, - isStacked: action.payload.isStacked, - nullZeroValues: action.payload.nullZeroValues, - opacity: action.payload.opacity, - timePreferance: action.payload.timePreferance, - title: action.payload.title, - }, - ...afterWidget, - ], - }, - }, - ], - }; - } - - case SAVE_SETTING_TO_PANEL_SUCCESS: - case UPDATE_DASHBOARD: { - const selectedDashboard = action.payload; - - return { - ...state, - dashboards: [ - { - ...selectedDashboard, - }, - ], - }; - } - - case FLUSH_DASHBOARD: { - return { - ...state, - dashboards: [], - }; - } - case DELETE_WIDGET_SUCCESS: { - const { widgetId, layout } = action.payload; - - const { dashboards } = state; - const [selectedDashboard] = dashboards; - const { data } = selectedDashboard; - const { widgets = [] } = data; - - return { - ...state, - dashboards: [ - { - ...selectedDashboard, - data: { - ...data, - widgets: widgets.filter((e) => e.id !== widgetId), - layout, - }, - }, - ], - }; - } - - case IS_ADD_WIDGET: { - return { - ...state, - isAddWidget: action.payload.isAddWidget, - }; - } - - case UPDATE_QUERY: { - const { widgetId, yAxisUnit } = 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, - yAxisUnit, - }, - ...afterWidget, - ], - }, - }, - ], - }; - } - case UPDATE_DASHBOARD_VARIABLES: { - const variablesData = action.payload; - const { dashboards } = state; - const [selectedDashboard] = dashboards; - const { data } = selectedDashboard; - - return { - ...state, - dashboards: [ - { - ...selectedDashboard, - data: { - ...data, - variables: variablesData, - }, - }, - ], - }; - } - default: - return state; - } -}; - -export default dashboard; diff --git a/frontend/src/store/reducers/index.ts b/frontend/src/store/reducers/index.ts index bfb7a357e1..b685ff6412 100644 --- a/frontend/src/store/reducers/index.ts +++ b/frontend/src/store/reducers/index.ts @@ -1,7 +1,6 @@ import { combineReducers } from 'redux'; import appReducer from './app'; -import dashboardReducer from './dashboard'; import globalTimeReducer from './global'; import { LogsReducer } from './logs'; import metricsReducers from './metric'; @@ -14,7 +13,6 @@ const reducers = combineReducers({ usageDate: usageDataReducer, globalTime: globalTimeReducer, serviceMap: ServiceMapReducer, - dashboards: dashboardReducer, app: appReducer, metrics: metricsReducers, logs: LogsReducer, diff --git a/frontend/src/types/actions/dashboard.ts b/frontend/src/types/actions/dashboard.ts deleted file mode 100644 index a147eb7516..0000000000 --- a/frontend/src/types/actions/dashboard.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { Layout } from 'react-grid-layout'; -import { ApplySettingsToPanelProps } from 'store/actions/dashboard/applySettingsToPanel'; -import { - Dashboard, - IDashboardVariable, - Widgets, -} from 'types/api/dashboard/getAll'; -import { QueryData } from 'types/api/widgets/getQuery'; - -export const GET_DASHBOARD = 'GET_DASHBOARD'; -export const UPDATE_DASHBOARD = 'UPDATE_DASHBOARD'; - -export const GET_ALL_DASHBOARD_LOADING_START = - 'GET_ALL_DASHBOARD_LOADING_START'; -export const GET_ALL_DASHBOARD_SUCCESS = 'GET_ALL_DASHBOARD_SUCCESS'; -export const GET_ALL_DASHBOARD_ERROR = 'GET_ALL_DASHBOARD_ERROR'; - -export const GET_DASHBOARD_LOADING_START = 'GET_DASHBOARD_LOADING_START'; -export const GET_DASHBOARD_SUCCESS = 'GET_DASHBOARD_SUCCESS'; -export const GET_DASHBOARD_ERROR = 'GET_DASHBOARD_ERROR'; -export const UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS = - 'UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS'; -export const UPDATE_TITLE_DESCRIPTION_TAGS_ERROR = - 'UPDATE_TITLE_DESCRIPTION_TAGS_ERROR'; -export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'; - -export const DELETE_DASHBOARD_SUCCESS = 'DELETE_DASHBOARD_SUCCESS'; -export const DELETE_DASHBOARD_ERROR = 'DELETE_DASHBOARD_ERROR'; - -export const CREATE_DEFAULT_WIDGET = 'CREATE_DEFAULT_WIDGET'; - -export const UPDATE_QUERY = 'UPDATE_QUERY'; - -export const APPLY_SETTINGS_TO_PANEL = 'APPLY_SETTINGS_TO_PANEL'; - -export const SAVE_SETTING_TO_PANEL_SUCCESS = 'SAVE_SETTING_TO_PANEL_SUCCESS'; -export const SAVE_SETTING_TO_PANEL_ERROR = 'SAVE_SETTING_TO_PANEL_ERROR'; - -export const DELETE_WIDGET_SUCCESS = 'DELETE_WIDGET_SUCCESS'; -export const DELETE_WIDGET_ERROR = 'DELETE_WIDGET_ERROR'; - -export const IS_ADD_WIDGET = 'IS_ADD_WIDGET'; - -export const DELETE_QUERY = 'DELETE_QUERY'; -export const FLUSH_DASHBOARD = 'FLUSH_DASHBOARD'; -export const UPDATE_DASHBOARD_VARIABLES = 'UPDATE_DASHBOARD_VARIABLES'; - -interface GetDashboard { - type: typeof GET_DASHBOARD; - payload: Dashboard; -} -interface UpdateDashboard { - type: typeof UPDATE_DASHBOARD; - payload: Dashboard; -} - -interface DeleteDashboardSuccess { - type: typeof DELETE_DASHBOARD_SUCCESS; - payload: { - uuid: Dashboard['uuid']; - }; -} - -interface DashboardStart { - type: - | typeof GET_ALL_DASHBOARD_LOADING_START - | typeof GET_DASHBOARD_LOADING_START; -} - -interface GetAllDashboardSuccess { - type: typeof GET_ALL_DASHBOARD_SUCCESS; - payload: Dashboard[]; -} - -interface GetDashboardSuccess { - type: typeof GET_DASHBOARD_SUCCESS; - payload: Dashboard; -} - -interface ApplySettingsToPanel { - type: typeof APPLY_SETTINGS_TO_PANEL; - payload: ApplySettingsToPanelProps; -} - -interface CreateDefaultWidget { - type: typeof CREATE_DEFAULT_WIDGET; - payload: Widgets; -} - -interface IsAddWidget { - type: typeof IS_ADD_WIDGET; - payload: { - isAddWidget: boolean; - }; -} - -interface DashboardError { - type: - | typeof GET_ALL_DASHBOARD_ERROR - | typeof GET_DASHBOARD_ERROR - | typeof UPDATE_TITLE_DESCRIPTION_TAGS_ERROR - | typeof DELETE_DASHBOARD_ERROR - | typeof SAVE_SETTING_TO_PANEL_ERROR - | typeof DELETE_WIDGET_ERROR; - payload: { - errorMessage: string; - }; -} - -interface UpdateDashboardTitle { - type: typeof UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS; - payload: { - title: Dashboard['data']['title']; - description: Dashboard['data']['description']; - tags: Dashboard['data']['tags']; - }; -} - -interface ToggleEditMode { - type: typeof TOGGLE_EDIT_MODE; -} - -export interface QuerySuccessPayload { - widgetId: string; - data: { - // legend: string; - queryData: QueryData[]; - // query: string - }; -} - -interface UpdateQuery { - type: typeof UPDATE_QUERY; - payload: { - widgetId: string; - yAxisUnit: string | undefined; - }; -} - -interface SaveDashboardSuccess { - type: typeof SAVE_SETTING_TO_PANEL_SUCCESS; - payload: Dashboard; -} - -interface WidgetDeleteSuccess { - type: typeof DELETE_WIDGET_SUCCESS; - payload: { - widgetId: Widgets['id']; - layout: Layout[]; - }; -} - -export interface DeleteQueryProps { - widgetId: string; - currentIndex: number; -} - -interface DeleteQuery { - type: typeof DELETE_QUERY; - payload: DeleteQueryProps; -} - -interface FlushDashboard { - type: typeof FLUSH_DASHBOARD; -} -interface UpdateDashboardVariables { - type: typeof UPDATE_DASHBOARD_VARIABLES; - payload: Record; -} - -export type DashboardActions = - | GetDashboard - | UpdateDashboard - | DeleteDashboardSuccess - | DashboardError - | GetAllDashboardSuccess - | DashboardStart - | GetDashboardSuccess - | UpdateDashboardTitle - | ToggleEditMode - | CreateDefaultWidget - | ApplySettingsToPanel - | SaveDashboardSuccess - | WidgetDeleteSuccess - | IsAddWidget - | UpdateQuery - | DeleteQuery - | FlushDashboard - | UpdateDashboardVariables; diff --git a/frontend/src/types/actions/index.ts b/frontend/src/types/actions/index.ts index 5d15b0c150..723c6956bb 100644 --- a/frontend/src/types/actions/index.ts +++ b/frontend/src/types/actions/index.ts @@ -1,12 +1,10 @@ import { AppAction } from './app'; -import { DashboardActions } from './dashboard'; import { GlobalTimeAction } from './globalTime'; import { LogsActions } from './logs'; import { MetricsActions } from './metrics'; import { TraceActions } from './trace'; type AppActions = - | DashboardActions | AppAction | GlobalTimeAction | MetricsActions diff --git a/frontend/src/types/api/dashboard/delete.ts b/frontend/src/types/api/dashboard/delete.ts index 78d3253d30..9bdc519b47 100644 --- a/frontend/src/types/api/dashboard/delete.ts +++ b/frontend/src/types/api/dashboard/delete.ts @@ -3,3 +3,7 @@ import { Dashboard } from './getAll'; export type Props = { uuid: Dashboard['uuid']; }; + +export interface PayloadProps { + status: 'success'; +} diff --git a/frontend/src/types/api/settings/ingestion.ts b/frontend/src/types/api/settings/ingestion.ts new file mode 100644 index 0000000000..cffd74a696 --- /dev/null +++ b/frontend/src/types/api/settings/ingestion.ts @@ -0,0 +1,18 @@ +export interface IngestionInfo { + keyId: string; + name: string; + createdAt: string; + ingestionKey: string; + ingestionURL: string; + dataRegion: string; +} + +export interface IngestionResponseProps { + payload: IngestionInfo[]; +} + +export interface IngestionDataType { + key: string; + name: string; + value: string; +} diff --git a/frontend/src/types/reducer/dashboards.ts b/frontend/src/types/reducer/dashboards.ts deleted file mode 100644 index a8d5bb2bae..0000000000 --- a/frontend/src/types/reducer/dashboards.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PayloadProps } from 'types/api/dashboard/getAll'; - -export default interface DashboardReducer { - dashboards: PayloadProps; - loading: boolean; - error: boolean; - errorMessage: string; - isEditMode: boolean; - isAddWidget: boolean; -} diff --git a/frontend/src/utils/dashboard/selectedDashboard.ts b/frontend/src/utils/dashboard/selectedDashboard.ts deleted file mode 100644 index c05077f39c..0000000000 --- a/frontend/src/utils/dashboard/selectedDashboard.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; - -export const getSelectedDashboard = (dashboard: Dashboard[]): Dashboard => { - if (dashboard.length > 0) { - return dashboard[0]; - } - return {} as Dashboard; -}; - -export const getSelectedDashboardVariable = ( - dashboard: Dashboard[], -): Record => { - if (dashboard.length > 0) { - const { variables } = dashboard[0].data; - return variables; - } - return {}; -}; diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 1dabfb255b..7b9b82bae7 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -46,6 +46,7 @@ export const routePermission: Record = { MY_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'], SERVICE_MAP: ['ADMIN', 'EDITOR', 'VIEWER'], ALL_CHANNELS: ['ADMIN', 'EDITOR', 'VIEWER'], + INGESTION_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'], ALL_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'], ALL_ERROR: ['ADMIN', 'EDITOR', 'VIEWER'], APPLICATION: ['ADMIN', 'EDITOR', 'VIEWER'], diff --git a/go.mod b/go.mod index 2402e92498..d4bba0fd35 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974 - github.com/antonmedv/expr v1.12.4 + github.com/antonmedv/expr v1.12.5 github.com/auth0/go-jwt-middleware v1.0.1 github.com/cespare/xxhash v1.1.0 github.com/coreos/go-oidc/v3 v3.4.0 @@ -31,13 +31,15 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/oklog/oklog v0.3.2 github.com/open-telemetry/opamp-go v0.5.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.79.0 + github.com/open-telemetry/opentelemetry-collector-contrib/processor/logstransformprocessor v0.79.0 github.com/opentracing/opentracing-go v1.2.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f github.com/prometheus/common v0.44.0 github.com/prometheus/prometheus v2.5.0+incompatible - github.com/rs/cors v1.8.2 + github.com/rs/cors v1.9.0 github.com/russellhaering/gosaml2 v0.9.0 github.com/russellhaering/goxmldsig v1.2.0 github.com/samber/lo v1.38.1 @@ -47,14 +49,20 @@ require ( github.com/soheilhy/cmux v0.1.5 github.com/srikanthccv/ClickHouse-go-mock v0.4.0 github.com/stretchr/testify v1.8.4 - go.opentelemetry.io/collector/confmap v0.70.0 + go.opentelemetry.io/collector v0.79.0 + go.opentelemetry.io/collector/component v0.79.0 + go.opentelemetry.io/collector/confmap v0.79.0 + go.opentelemetry.io/collector/consumer v0.79.0 + go.opentelemetry.io/collector/exporter v0.79.0 + go.opentelemetry.io/collector/pdata v1.0.0-rcv0014 + go.opentelemetry.io/collector/receiver v0.79.0 go.opentelemetry.io/otel v1.17.0 go.opentelemetry.io/otel/sdk v1.16.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.25.0 - golang.org/x/crypto v0.11.0 + golang.org/x/crypto v0.14.0 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 - golang.org/x/net v0.13.0 + golang.org/x/net v0.17.0 golang.org/x/oauth2 v0.10.0 google.golang.org/grpc v1.57.0 google.golang.org/protobuf v1.31.0 @@ -65,6 +73,7 @@ require ( ) require ( + contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect @@ -77,6 +86,7 @@ require ( github.com/beevik/etree v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect @@ -89,8 +99,10 @@ require ( github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect @@ -98,6 +110,7 @@ require ( github.com/gosimple/unidecode v1.0.0 // indirect github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -106,6 +119,7 @@ require ( github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/cpuid v1.2.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/minio/md5-simd v1.1.0 // indirect @@ -116,36 +130,52 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/observiq/ctimefmt v1.0.0 // indirect github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.79.0 // indirect github.com/paulmach/orb v0.10.0 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/procfs v0.11.0 // indirect + github.com/prometheus/statsd_exporter v0.22.7 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/backo-go v1.0.1 // indirect + github.com/shirou/gopsutil/v3 v3.23.4 // indirect + github.com/shoenig/go-m1cpu v0.1.5 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/smarty/assertions v1.15.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect - go.opentelemetry.io/collector/featuregate v0.70.0 // indirect - go.opentelemetry.io/collector/pdata v1.0.0-rcv0014 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/collector/featuregate v1.0.0-rcv0012 // indirect go.opentelemetry.io/collector/semconv v0.81.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect + go.opentelemetry.io/otel/bridge/opencensus v0.39.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.39.0 // indirect go.opentelemetry.io/otel/metric v1.17.0 // indirect + go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect go.opentelemetry.io/otel/trace v1.17.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/goleak v1.2.1 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect + gonum.org/v1/gonum v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230717213848-3f92550aa753 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230717213848-3f92550aa753 // indirect diff --git a/go.sum b/go.sum index 6283ae2486..b85b7f25aa 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= +contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw= github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -90,6 +92,7 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Mottl/ctimefmt v0.0.0-20190803144728-fd2ac23a585a/go.mod h1:eyj2WSIdoPMPs2eNTLpSmM6Nzqo4V80/d6jHpnJ1SAI= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb h1:bneLSKPf9YUSFmafKx32bynV6QrzViL/s+ZDvQxH1E4= @@ -112,8 +115,8 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8V github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antonmedv/expr v1.12.4 h1:YRkeF7r0cejMS47bDYe3Jyes7L9t1AhpunC+Duq+R9k= -github.com/antonmedv/expr v1.12.4/go.mod h1:FPC8iWArxls7axbVLsW+kpg1mz29A1b2M6jt+hZfDkU= +github.com/antonmedv/expr v1.12.5 h1:Fq4okale9swwL3OeLLs9WD9H6GbgBLJyN/NUHRv+n0E= +github.com/antonmedv/expr v1.12.5/go.mod h1:FPC8iWArxls7axbVLsW+kpg1mz29A1b2M6jt+hZfDkU= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= @@ -146,6 +149,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -173,6 +178,7 @@ github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -244,12 +250,14 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -258,6 +266,8 @@ 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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= @@ -291,6 +301,8 @@ github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -467,8 +479,10 @@ github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEF github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ionos-cloud/sdk-go/v6 v6.1.8 h1:493wE/BkZxJf7x79UCE0cYGPZoqQcPiEBALvt7uVGY0= github.com/ionos-cloud/sdk-go/v6 v6.1.8/go.mod h1:EzEgRIDxBELvfoa/uBN0kOQaqovLjUWEB7iW4/Q+t4k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -527,6 +541,8 @@ github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/linode/linodego v1.19.0 h1:n4WJrcr9+30e9JGZ6DI0nZbm5SdAj1kSwvvt/998YUw= github.com/linode/linodego v1.19.0/go.mod h1:XZFR+yJ9mm2kwf6itZ6SCpu+6w3KnIevV0Uu5HNWJgQ= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= @@ -593,6 +609,8 @@ github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnu github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/observiq/ctimefmt v1.0.0 h1:r7vTJ+Slkrt9fZ67mkf+mA6zAdR5nGIJRMTzkUyvilk= +github.com/observiq/ctimefmt v1.0.0/go.mod h1:mxi62//WbSpG/roCO1c6MqZ7zQTvjVtYheqHN3eOjvc= github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= @@ -614,6 +632,18 @@ github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= github.com/open-telemetry/opamp-go v0.5.0 h1:2YFbb6G4qBkq3yTRdVb5Nfz9hKHW/ldUyex352e1J7g= github.com/open-telemetry/opamp-go v0.5.0/go.mod h1:IMdeuHGVc5CjKSu5/oNV0o+UmiXuahoHvoZ4GOmAI9M= +github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.79.0 h1:okH5+f5UyufgnQXcBib2qUaKoTwsETWxdO0VJOXdl6s= +github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.79.0/go.mod h1:uGs9o7HgZXhRIgdHWT+VreU2ew4a08KTU4ejQIw6dTM= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.79.0 h1:OZPeakqoSZ1yRlmGBlWi9kISx/9PJzlNLGLutFPOQY0= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.79.0/go.mod h1:VOHKYi1wm+/c2wZA3mY1Grd4eYP8uS//EV0yHBbGfGw= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.79.0 h1:YtkbJjknfMJ4UKXdaUBTw37QLxYXuVMmozqPFTM0XPI= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.79.0/go.mod h1:BFqnTfM5Neh5A5gYtsxh2615yp/t39vUj7oRUcjevFU= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.79.0 h1:Cao5mD7NFJsMLM40lisqJ4Iy3+v+JOq+8cR79/Uki94= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.79.0/go.mod h1:/wfeJfzu3oAkC2boitFR3dZcnwNtwzryI/SW1LIhDLo= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.79.0 h1:o1aUgN0pA5Sc0s2bOUy7vDoNyJ6D6qdHihXk3BKyf58= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.79.0/go.mod h1:t8I2umZdg81AQmncs7fVHw1YMzSol3A7ecsc2lfqgaM= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/logstransformprocessor v0.79.0 h1:EpuwiWvq1hqS4PAp/+kMvWVkM4o+PRGtTGSDLpmIeME= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/logstransformprocessor v0.79.0/go.mod h1:0dccj1BrKVG00hvt2f70tu7Re1YjAl5Jpy2lduSrLnI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= @@ -629,8 +659,9 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s= github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= -github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -648,11 +679,16 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f h1:h0p1aZ9F5d6IXOygysob3g4B07b+HuVUQC0VJKD8wA4= github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -665,6 +701,9 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= @@ -673,8 +712,12 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk= github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= +github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= @@ -685,13 +728,14 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= -github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= +github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russellhaering/gosaml2 v0.9.0 h1:CNMnH42z/GirrKjdmNrSS6bAAs47F9bPdl4PfRmVOIk= github.com/russellhaering/gosaml2 v0.9.0/go.mod h1:byViER/1YPUa0Puj9ROZblpoq2jsE7h/CJmitzX0geU= github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80guqzwx6Vg= github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= @@ -706,6 +750,12 @@ github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N+ github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= +github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o= +github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8= +github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ= +github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ= +github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c= +github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -729,6 +779,8 @@ 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 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/srikanthccv/ClickHouse-go-mock v0.4.0 h1:tLk7qoDLg7Z5YD5mOmNqjRDbsm6ehJVXOFvSnG+gQAg= @@ -746,10 +798,16 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= @@ -767,6 +825,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= @@ -778,22 +838,46 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/collector/confmap v0.70.0 h1:GJDaM7c3yFyT7Zv6l2/5ahwaqPCvtC92Ii8Bg2AVdjU= -go.opentelemetry.io/collector/confmap v0.70.0/go.mod h1:8//JWR2TMChLH35Az0mGFrCskEIP6POgZJK6iRRhzeM= -go.opentelemetry.io/collector/featuregate v0.70.0 h1:Xr6hrMT/++SjTm06nreex8WlpgFhYJ7S0yRVn1OvVf8= -go.opentelemetry.io/collector/featuregate v0.70.0/go.mod h1:ih+oCwrHW3bLac/qnPUzes28yDCDmh8WzsAKKauwCYI= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/collector v0.79.0 h1:Lra7U0ilMor1g5WVkO3YZ0kZYsvzAtGN+Uq+CmC96JY= +go.opentelemetry.io/collector v0.79.0/go.mod h1:O2Vfwykphq9VqdATZiAypjnJMS3WFBXwFSe/0ujo38Q= +go.opentelemetry.io/collector/component v0.79.0 h1:ZKLJ4qa0AngmyGp1RQBJgl6OIP6mxdfrVpbz09h/W34= +go.opentelemetry.io/collector/component v0.79.0/go.mod h1:rX0gixMemcXZTZaML5zUiT+5txZUYkWnACscJkFVj18= +go.opentelemetry.io/collector/confmap v0.79.0 h1:a4XVde3lLP81BiSbt8AzVD6pvQBX8YkrB9ZtMSHKv1A= +go.opentelemetry.io/collector/confmap v0.79.0/go.mod h1:cKr2c7lVtEJCuMOncUPlcROJBbTFaHiPjYp1Y8RbL+Q= +go.opentelemetry.io/collector/consumer v0.79.0 h1:V/4PCvbTw2Bt+lYb/ogac0g/nCCb3oKnmz+jM3t5Dyk= +go.opentelemetry.io/collector/consumer v0.79.0/go.mod h1:VfqIyUI5K20zXx3mfVN+skmA+V3sV5fNorJ5TaIOj/U= +go.opentelemetry.io/collector/exporter v0.79.0 h1:PxhKgWf1AkZvN1PjiJT5xiO+pKZA9Y4fyuMs5aNFuEA= +go.opentelemetry.io/collector/exporter v0.79.0/go.mod h1:qlXiqnOUeHelpAwk03f8nB5+91UIqlA7udSBsj9bJ3M= +go.opentelemetry.io/collector/extension/zpagesextension v0.79.0 h1:I1DIomwNnNecBBSKLR3O0OlEqbPNCtMw48tAfj39VKM= +go.opentelemetry.io/collector/extension/zpagesextension v0.79.0/go.mod h1:zlgkyg7I4Hu0ZahSjlTl2RBrju744PcPyQ7IDDCFQWI= +go.opentelemetry.io/collector/featuregate v1.0.0-rcv0012 h1:pSO81lfikGEgRXHepmOGy2o6WWCly427UJCgMJC5c8g= +go.opentelemetry.io/collector/featuregate v1.0.0-rcv0012/go.mod h1:/kVAsGUCyJXIDSgHftCN63QiwAEVHRLX2Kh/S+dqgHY= go.opentelemetry.io/collector/pdata v1.0.0-rcv0014 h1:iT5qH0NLmkGeIdDtnBogYDx7L58t6CaWGL378DEo2QY= go.opentelemetry.io/collector/pdata v1.0.0-rcv0014/go.mod h1:BRvDrx43kiSoUx3mr7SoA7h9B8+OY99mUK+CZSQFWW4= +go.opentelemetry.io/collector/receiver v0.79.0 h1:Ag4hciAYklQWDpKbnmqhfh9zJlUskWvThpCpphp12b4= +go.opentelemetry.io/collector/receiver v0.79.0/go.mod h1:+/xe0VoYl6Mli+KQTZWBR2apqFsbioAAqu7abzKDskI= go.opentelemetry.io/collector/semconv v0.81.0 h1:lCYNNo3powDvFIaTPP2jDKIrBiV1T92NK4QgL/aHYXw= go.opentelemetry.io/collector/semconv v0.81.0/go.mod h1:TlYPtzvsXyHOgr5eATi43qEMqwSmIziivJB2uctKswo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8= +go.opentelemetry.io/contrib/propagators/b3 v1.17.0 h1:ImOVvHnku8jijXqkwCSyYKRDt2YrnGXD4BbhcpfbfJo= +go.opentelemetry.io/contrib/propagators/b3 v1.17.0/go.mod h1:IkfUfMpKWmynvvE0264trz0sf32NRTZL4nuAN9AbWRc= +go.opentelemetry.io/contrib/zpages v0.42.0 h1:hFscXKQ9PTjyIVmAr6zIV8cMoiEeR9lPIwPVqHi8+5Q= +go.opentelemetry.io/contrib/zpages v0.42.0/go.mod h1:qRJBEfB0iwRKrYImq5qfwTolmY8HXvZBRucvhuTVQZw= go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= +go.opentelemetry.io/otel/bridge/opencensus v0.39.0 h1:YHivttTaDhbZIHuPlg1sWsy2P5gj57vzqPfkHItgbwQ= +go.opentelemetry.io/otel/bridge/opencensus v0.39.0/go.mod h1:vZ4537pNjFDXEx//WldAR6Ro2LC8wwmFC76njAXwNPE= +go.opentelemetry.io/otel/exporters/prometheus v0.39.0 h1:whAaiHxOatgtKd+w0dOi//1KUxj3KoPINZdtDaDj3IA= +go.opentelemetry.io/otel/exporters/prometheus v0.39.0/go.mod h1:4jo5Q4CROlCpSPsXLhymi+LYrDXd2ObU5wbKayfZs7Y= go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= +go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= +go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= @@ -821,8 +905,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -919,8 +1003,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.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= -golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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= @@ -977,6 +1061,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1008,6 +1093,7 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1034,6 +1120,7 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1043,17 +1130,20 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.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.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 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= @@ -1065,8 +1155,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.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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= @@ -1138,6 +1228,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.13.0 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM= +gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1320,6 +1412,7 @@ 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/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= diff --git a/pkg/query-service/Dockerfile b/pkg/query-service/Dockerfile index e616ad5d12..9d62c5cc62 100644 --- a/pkg/query-service/Dockerfile +++ b/pkg/query-service/Dockerfile @@ -1,45 +1,20 @@ -FROM golang:1.21-bookworm AS builder - -# LD_FLAGS is passed as argument from Makefile. It will be empty, if no argument passed -ARG LD_FLAGS -ARG TARGETPLATFORM - -ENV CGO_ENABLED=1 -ENV GOPATH=/go - -RUN export GOOS=$(echo ${TARGETPLATFORM} | cut -d / -f1) && \ - export GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) - -# Prepare and enter src directory -WORKDIR /go/src/github.com/signoz/signoz - -# Cache dependencies -ADD go.mod . -ADD go.sum . -RUN go mod download -x - -# Add the sources and proceed with build -ADD . . -RUN cd pkg/query-service \ - && go build -tags timetzdata -a -o ./bin/query-service \ - -ldflags "-linkmode external -extldflags '-static' -s -w $LD_FLAGS" \ - && chmod +x ./bin/query-service - - # use a minimal alpine image -FROM alpine:3.16.7 +FROM alpine:3.17 # Add Maintainer Info LABEL maintainer="signoz" +# define arguments that can be passed during build time +ARG TARGETOS TARGETARCH + # add ca-certificates in case you need them RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* # set working directory WORKDIR /root -# copy the binary from builder -COPY --from=builder /go/src/github.com/signoz/signoz/pkg/query-service/bin/query-service . +# copy the query-service binary +COPY pkg/query-service/bin/query-service-${TARGETOS}-${TARGETARCH} /root/query-service # copy prometheus YAML config COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml @@ -51,6 +26,5 @@ RUN chmod 755 /root /root/query-service ENTRYPOINT ["./query-service"] CMD ["-config", "/root/config/prometheus.yml"] -# CMD ["./query-service -config /root/config/prometheus.yml"] EXPOSE 8080 diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 4625aa226b..ffea2a9b7e 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -3581,8 +3581,8 @@ func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.Upda return nil } -func (r *ClickHouseReader) GetLogs(ctx context.Context, params *model.LogsFilterParams) (*[]model.GetLogsResponse, *model.ApiError) { - response := []model.GetLogsResponse{} +func (r *ClickHouseReader) GetLogs(ctx context.Context, params *model.LogsFilterParams) (*[]model.SignozLog, *model.ApiError) { + response := []model.SignozLog{} fields, apiErr := r.GetLogFields(ctx) if apiErr != nil { return nil, apiErr @@ -3678,7 +3678,7 @@ func (r *ClickHouseReader) TailLogs(ctx context.Context, client *model.LogsTailC } tmpQuery = fmt.Sprintf("%s order by timestamp desc, id desc limit 100", tmpQuery) zap.S().Debug(tmpQuery) - response := []model.GetLogsResponse{} + response := []model.SignozLog{} err := r.db.Select(ctx, &response, tmpQuery) if err != nil { zap.S().Error(err) @@ -4702,7 +4702,7 @@ func (r *ClickHouseReader) LiveTailLogsV3(ctx context.Context, query string, tim tmpQuery = query + tmpQuery + " order by timestamp desc, id desc limit 100" // using the old structure since we can directly read it to the struct as use it. - response := []model.GetLogsResponse{} + response := []model.SignozLog{} err := r.db.Select(ctx, &response, tmpQuery) if err != nil { zap.S().Error(err) diff --git a/pkg/query-service/app/dashboards/model.go b/pkg/query-service/app/dashboards/model.go index 2403775cb8..e97facff62 100644 --- a/pkg/query-service/app/dashboards/model.go +++ b/pkg/query-service/app/dashboards/model.go @@ -147,8 +147,9 @@ func CreateDashboard(data map[string]interface{}, fm interfaces.FeatureLookup) ( return nil, &model.ApiError{Typ: model.ErrorExec, Err: err} } - if countTraceAndLogsPanel(data) > 0 { - fErr := checkFeatureUsage(fm, countTraceAndLogsPanel(data)) + newCount, _ := countTraceAndLogsPanel(data) + if newCount > 0 { + fErr := checkFeatureUsage(fm, newCount) if fErr != nil { return nil, fErr } @@ -168,7 +169,7 @@ func CreateDashboard(data map[string]interface{}, fm interfaces.FeatureLookup) ( } dash.Id = int(lastInsertId) - traceAndLogsPanelUsage := countTraceAndLogsPanel(data) + traceAndLogsPanelUsage, _ := countTraceAndLogsPanel(data) if traceAndLogsPanelUsage > 0 { updateFeatureUsage(fm, traceAndLogsPanelUsage) } @@ -213,7 +214,7 @@ func DeleteDashboard(uuid string, fm interfaces.FeatureLookup) *model.ApiError { return &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no dashboard found with uuid: %s", uuid)} } - traceAndLogsPanelUsage := countTraceAndLogsPanel(dashboard.Data) + traceAndLogsPanelUsage, _ := countTraceAndLogsPanel(dashboard.Data) if traceAndLogsPanelUsage > 0 { updateFeatureUsage(fm, -traceAndLogsPanelUsage) } @@ -248,8 +249,8 @@ func UpdateDashboard(uuid string, data map[string]interface{}, fm interfaces.Fea } // check if the count of trace and logs QB panel has changed, if yes, then check feature flag count - existingCount := countTraceAndLogsPanel(dashboard.Data) - newCount := countTraceAndLogsPanel(data) + existingCount, existingTotal := countTraceAndLogsPanel(dashboard.Data) + newCount, newTotal := countTraceAndLogsPanel(data) if newCount > existingCount { err := checkFeatureUsage(fm, newCount-existingCount) if err != nil { @@ -257,6 +258,12 @@ func UpdateDashboard(uuid string, data map[string]interface{}, fm interfaces.Fea } } + if existingTotal > newTotal && existingTotal-newTotal > 1 { + // if the total count of panels has reduced by more than 1, + // return error + return nil, model.BadRequest(fmt.Errorf("deleting more than one panel is not supported")) + } + dashboard.UpdatedAt = time.Now() dashboard.Data = data @@ -588,8 +595,9 @@ func TransformGrafanaJSONToSignoz(grafanaJSON model.GrafanaJSON) model.Dashboard return toReturn } -func countTraceAndLogsPanel(data map[string]interface{}) int64 { +func countTraceAndLogsPanel(data map[string]interface{}) (int64, int64) { count := int64(0) + totalPanels := int64(0) if data != nil && data["widgets"] != nil { widgets, ok := data["widgets"].(interface{}) if ok { @@ -598,6 +606,7 @@ func countTraceAndLogsPanel(data map[string]interface{}) int64 { for _, widget := range data { sData, ok := widget.(map[string]interface{}) if ok && sData["query"] != nil { + totalPanels++ query, ok := sData["query"].(interface{}).(map[string]interface{}) if ok && query["queryType"] == "builder" && query["builder"] != nil { builderData, ok := query["builder"].(interface{}).(map[string]interface{}) @@ -620,5 +629,5 @@ func countTraceAndLogsPanel(data map[string]interface{}) int64 { } } } - return count + return count, totalPanels } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 0729338158..87f05ab098 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -377,6 +377,8 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) { router.HandleFunc("/api/v1/settings/ttl", am.ViewAccess(aH.getTTL)).Methods(http.MethodGet) router.HandleFunc("/api/v1/settings/apdex", am.AdminAccess(aH.setApdexSettings)).Methods(http.MethodPost) router.HandleFunc("/api/v1/settings/apdex", am.ViewAccess(aH.getApdexSettings)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/settings/ingestion_key", am.AdminAccess(aH.insertIngestionKey)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/settings/ingestion_key", am.ViewAccess(aH.getIngestionKeys)).Methods(http.MethodGet) router.HandleFunc("/api/v1/metric_meta", am.ViewAccess(aH.getLatencyMetricMetadata)).Methods(http.MethodGet) @@ -2318,6 +2320,7 @@ func (aH *APIHandler) RegisterLogsRoutes(router *mux.Router, am *AuthMiddleware) subRouter.HandleFunc("/aggregate", am.ViewAccess(aH.logAggregate)).Methods(http.MethodGet) // log pipelines + subRouter.HandleFunc("/pipelines/preview", am.ViewAccess(aH.PreviewLogsPipelinesHandler)).Methods(http.MethodPost) subRouter.HandleFunc("/pipelines/{version}", am.ViewAccess(aH.ListLogsPipelinesHandler)).Methods(http.MethodGet) subRouter.HandleFunc("/pipelines", am.EditAccess(aH.CreateLogsPipeline)).Methods(http.MethodPost) } @@ -2379,7 +2382,7 @@ func (aH *APIHandler) tailLogs(w http.ResponseWriter, r *http.Request) { } // create the client - client := &model.LogsTailClient{Name: r.RemoteAddr, Logs: make(chan *model.GetLogsResponse, 1000), Done: make(chan *bool), Error: make(chan error), Filter: *params} + client := &model.LogsTailClient{Name: r.RemoteAddr, Logs: make(chan *model.SignozLog, 1000), Done: make(chan *bool), Error: make(chan error), Filter: *params} go aH.reader.TailLogs(r.Context(), client) w.Header().Set("Connection", "keep-alive") @@ -2453,6 +2456,26 @@ func parseAgentConfigVersion(r *http.Request) (int, *model.ApiError) { return int(version64), nil } +func (ah *APIHandler) PreviewLogsPipelinesHandler(w http.ResponseWriter, r *http.Request) { + req := logparsingpipeline.PipelinesPreviewRequest{} + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + + resultLogs, apiErr := ah.LogsParsingPipelineController.PreviewLogsPipelines( + r.Context(), &req, + ) + + if apiErr != nil { + RespondError(w, apiErr, nil) + return + } + + ah.Respond(w, resultLogs) +} + func (ah *APIHandler) ListLogsPipelinesHandler(w http.ResponseWriter, r *http.Request) { version, err := parseAgentConfigVersion(r) @@ -3160,7 +3183,7 @@ func (aH *APIHandler) liveTailLogs(w http.ResponseWriter, r *http.Request) { } // create the client - client := &v3.LogsLiveTailClient{Name: r.RemoteAddr, Logs: make(chan *model.GetLogsResponse, 1000), Done: make(chan *bool), Error: make(chan error)} + client := &v3.LogsLiveTailClient{Name: r.RemoteAddr, Logs: make(chan *model.SignozLog, 1000), Done: make(chan *bool), Error: make(chan error)} go aH.reader.LiveTailLogsV3(r.Context(), queryString, uint64(queryRangeParams.Start), "", client) w.Header().Set("Connection", "keep-alive") diff --git a/pkg/query-service/app/ingestion_key.go b/pkg/query-service/app/ingestion_key.go new file mode 100644 index 0000000000..036d3a3032 --- /dev/null +++ b/pkg/query-service/app/ingestion_key.go @@ -0,0 +1,33 @@ +package app + +import ( + "context" + "net/http" + + "go.signoz.io/signoz/pkg/query-service/dao" + "go.signoz.io/signoz/pkg/query-service/model" +) + +func (aH *APIHandler) insertIngestionKey(w http.ResponseWriter, r *http.Request) { + req, err := parseInsertIngestionKeyRequest(r) + if aH.HandleError(w, err, http.StatusBadRequest) { + return + } + + if err := dao.DB().InsertIngestionKey(context.Background(), req); err != nil { + RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + + aH.WriteJSON(w, r, map[string]string{"data": "ingestion key added successfully"}) +} + +func (aH *APIHandler) getIngestionKeys(w http.ResponseWriter, r *http.Request) { + ingestionKeys, err := dao.DB().GetIngestionKeys(context.Background()) + if err != nil { + RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + + aH.WriteJSON(w, r, ingestionKeys) +} diff --git a/pkg/query-service/app/logparsingpipeline/controller.go b/pkg/query-service/app/logparsingpipeline/controller.go index 2aa036b394..fc10047c36 100644 --- a/pkg/query-service/app/logparsingpipeline/controller.go +++ b/pkg/query-service/app/logparsingpipeline/controller.go @@ -140,3 +140,29 @@ func (ic *LogParsingPipelineController) GetPipelinesByVersion( Pipelines: pipelines, }, nil } + +type PipelinesPreviewRequest struct { + Pipelines []Pipeline `json:"pipelines"` + Logs []model.SignozLog `json:"logs"` +} + +type PipelinesPreviewResponse struct { + OutputLogs []model.SignozLog `json:"logs"` +} + +func (ic *LogParsingPipelineController) PreviewLogsPipelines( + ctx context.Context, + request *PipelinesPreviewRequest, +) (*PipelinesPreviewResponse, *model.ApiError) { + result, err := SimulatePipelinesProcessing( + ctx, request.Pipelines, request.Logs, + ) + + if err != nil { + return nil, err + } + + return &PipelinesPreviewResponse{ + OutputLogs: result, + }, nil +} diff --git a/pkg/query-service/app/logparsingpipeline/preview.go b/pkg/query-service/app/logparsingpipeline/preview.go new file mode 100644 index 0000000000..9ce6839b00 --- /dev/null +++ b/pkg/query-service/app/logparsingpipeline/preview.go @@ -0,0 +1,254 @@ +package logparsingpipeline + +import ( + "context" + "fmt" + "sort" + "strings" + "time" + + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/logstransformprocessor" + "github.com/pkg/errors" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/processor" + "go.signoz.io/signoz/pkg/query-service/collectorsimulator" + "go.signoz.io/signoz/pkg/query-service/model" + "gopkg.in/yaml.v3" +) + +func SimulatePipelinesProcessing( + ctx context.Context, + pipelines []Pipeline, + logs []model.SignozLog, +) ( + []model.SignozLog, *model.ApiError, +) { + + if len(pipelines) < 1 { + return logs, nil + } + + // Collector simulation does not guarantee that logs will come + // out in the same order as in the input. + // + // Add a temp attribute for sorting logs in simulation output + inputOrderAttribute := "__signoz_input_idx__" + for i := 0; i < len(logs); i++ { + if logs[i].Attributes_int64 == nil { + logs[i].Attributes_int64 = map[string]int64{} + } + logs[i].Attributes_int64[inputOrderAttribute] = int64(i) + } + simulatorInputPLogs := SignozLogsToPLogs(logs) + + // Simulate processing of logs through an otel collector + processorConfigs, err := collectorProcessorsForPipelines(pipelines) + if err != nil { + return nil, model.BadRequest(errors.Wrap( + err, "could not prepare otel processors for pipelines", + )) + } + + processorFactories, err := processor.MakeFactoryMap( + logstransformprocessor.NewFactory(), + ) + if err != nil { + return nil, model.InternalError(errors.Wrap( + err, "could not construct processor factory map", + )) + } + + // Pipelines translate to logtransformprocessors in otel collector config. + // Each logtransformprocessor (stanza) does its own batching with a flush + // interval of 100ms. So e2e processing time for logs grows linearly with + // the number of logtransformprocessors involved. + // See defaultFlushInterval at https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/stanza/adapter/emitter.go + // TODO(Raj): Remove this after flushInterval is exposed in logtransformprocessor config + timeout := time.Millisecond * time.Duration(len(processorConfigs)*100+100) + + outputPLogs, collectorErrs, apiErr := collectorsimulator.SimulateLogsProcessing( + ctx, + processorFactories, + processorConfigs, + simulatorInputPLogs, + timeout, + ) + collectorErrsText := strings.Join(collectorErrs, "\n") + if apiErr != nil { + return nil, model.WrapApiError(apiErr, fmt.Sprintf( + "could not simulate log pipelines processing.\nCollector errors: %s\n", collectorErrsText, + )) + } + + outputSignozLogs := PLogsToSignozLogs(outputPLogs) + + // Sort output logs by their order in the input and remove the temp ordering attribute + sort.Slice(outputSignozLogs, func(i, j int) bool { + iIdx := outputSignozLogs[i].Attributes_int64[inputOrderAttribute] + jIdx := outputSignozLogs[j].Attributes_int64[inputOrderAttribute] + return iIdx < jIdx + }) + for _, sigLog := range outputSignozLogs { + delete(sigLog.Attributes_int64, inputOrderAttribute) + } + + return outputSignozLogs, nil +} + +func collectorProcessorsForPipelines(pipelines []Pipeline) ( + []collectorsimulator.ProcessorConfig, error, +) { + processors, procNames, err := PreparePipelineProcessor(pipelines) + if err != nil { + return nil, err + } + + processorConfigs := []collectorsimulator.ProcessorConfig{} + for _, procName := range procNames { + // convert `Processor` structs to map[string]interface{} + procYaml, err := yaml.Marshal(processors[procName]) + if err != nil { + return nil, errors.Wrap(err, "could not marshal Processor struct") + } + var procConfRaw map[string]interface{} + err = yaml.Unmarshal(procYaml, &procConfRaw) + if err != nil { + return nil, errors.Wrap(err, "could not unmarshal proc yaml") + } + + processorConfigs = append(processorConfigs, collectorsimulator.ProcessorConfig{ + Name: procName, + Config: procConfRaw, + }) + } + + return processorConfigs, nil +} + +// plog doesn't contain an ID field. +// SignozLog.ID is stored as a log attribute in plogs for processing +// and gets hydrated back later. +const SignozLogIdAttr = "__signoz_log_id__" + +func SignozLogsToPLogs(logs []model.SignozLog) []plog.Logs { + result := []plog.Logs{} + + for _, log := range logs { + pl := plog.NewLogs() + rl := pl.ResourceLogs().AppendEmpty() + + resourceAttribs := rl.Resource().Attributes() + for k, v := range log.Resources_string { + resourceAttribs.PutStr(k, v) + } + + scopeLog := rl.ScopeLogs().AppendEmpty() + slRecord := scopeLog.LogRecords().AppendEmpty() + + slRecord.SetTimestamp(pcommon.NewTimestampFromTime( + time.Unix(0, int64(log.Timestamp)), + )) + + var traceIdBuf [16]byte + copy(traceIdBuf[:], []byte(log.TraceID)) + slRecord.SetTraceID(traceIdBuf) + + var spanIdBuf [8]byte + copy(spanIdBuf[:], []byte(log.SpanID)) + slRecord.SetSpanID(spanIdBuf) + + slRecord.SetFlags(plog.LogRecordFlags(log.TraceFlags)) + + slRecord.SetSeverityText(log.SeverityText) + slRecord.SetSeverityNumber(plog.SeverityNumber(log.SeverityNumber)) + + slRecord.Body().SetStr(log.Body) + + slAttribs := slRecord.Attributes() + for k, v := range log.Attributes_int64 { + slAttribs.PutInt(k, v) + } + for k, v := range log.Attributes_float64 { + slAttribs.PutDouble(k, v) + } + for k, v := range log.Attributes_string { + slAttribs.PutStr(k, v) + } + slAttribs.PutStr(SignozLogIdAttr, log.ID) + + result = append(result, pl) + } + + return result +} + +func PLogsToSignozLogs(plogs []plog.Logs) []model.SignozLog { + result := []model.SignozLog{} + + for _, pl := range plogs { + + resourceLogsSlice := pl.ResourceLogs() + for i := 0; i < resourceLogsSlice.Len(); i++ { + rl := resourceLogsSlice.At(i) + + scopeLogsSlice := rl.ScopeLogs() + for j := 0; j < scopeLogsSlice.Len(); j++ { + sl := scopeLogsSlice.At(j) + + lrSlice := sl.LogRecords() + for k := 0; k < lrSlice.Len(); k++ { + lr := lrSlice.At(k) + + // Recover ID for the log and remove temp attrib used for storing it + signozLogId := "" + logIdVal, exists := lr.Attributes().Get(SignozLogIdAttr) + if exists { + signozLogId = logIdVal.Str() + } + lr.Attributes().Remove(SignozLogIdAttr) + + signozLog := model.SignozLog{ + Timestamp: uint64(lr.Timestamp()), + ID: signozLogId, + TraceID: lr.TraceID().String(), + SpanID: lr.SpanID().String(), + TraceFlags: uint32(lr.Flags()), + SeverityText: lr.SeverityText(), + SeverityNumber: uint8(lr.SeverityNumber()), + Body: lr.Body().AsString(), + Resources_string: pMapToStrMap(rl.Resource().Attributes()), + Attributes_string: map[string]string{}, + Attributes_int64: map[string]int64{}, + Attributes_float64: map[string]float64{}, + } + + // Populate signozLog.Attributes_... + lr.Attributes().Range(func(k string, v pcommon.Value) bool { + if v.Type() == pcommon.ValueTypeDouble { + signozLog.Attributes_float64[k] = v.Double() + } else if v.Type() == pcommon.ValueTypeInt { + signozLog.Attributes_int64[k] = v.Int() + } else { + signozLog.Attributes_string[k] = v.AsString() + } + return true + }) + + result = append(result, signozLog) + } + } + } + } + + return result +} + +func pMapToStrMap(pMap pcommon.Map) map[string]string { + result := map[string]string{} + pMap.Range(func(k string, v pcommon.Value) bool { + result[k] = v.AsString() + return true + }) + return result +} diff --git a/pkg/query-service/app/logparsingpipeline/preview_test.go b/pkg/query-service/app/logparsingpipeline/preview_test.go new file mode 100644 index 0000000000..c453cb1445 --- /dev/null +++ b/pkg/query-service/app/logparsingpipeline/preview_test.go @@ -0,0 +1,158 @@ +package logparsingpipeline + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry" + "github.com/stretchr/testify/require" + "go.signoz.io/signoz/pkg/query-service/model" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" +) + +func TestPipelinePreview(t *testing.T) { + require := require.New(t) + + testPipelines := []Pipeline{ + { + OrderId: 1, + Name: "pipeline1", + Alias: "pipeline1", + Enabled: true, + Filter: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: "method", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + }, + Operator: "=", + Value: "GET", + }, + }, + }, + Config: []PipelineOperator{ + { + OrderId: 1, + ID: "add", + Type: "add", + Field: "attributes.test", + Value: "val", + Enabled: true, + Name: "test add", + }, + }, + }, + { + OrderId: 2, + Name: "pipeline2", + Alias: "pipeline2", + Enabled: true, + Filter: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: "method", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + }, + Operator: "=", + Value: "GET", + }, + }, + }, + Config: []PipelineOperator{ + { + OrderId: 1, + ID: "add", + Type: "add", + Field: "resource.test1", + Value: "val1", + Enabled: true, + Name: "test add2", + }, { + OrderId: 2, + ID: "add2", + Type: "add", + Field: "resource.test2", + Value: "val2", + Enabled: true, + Name: "test add3", + }, + }, + }, + } + + matchingLog := makeTestLogEntry( + "test log body", + map[string]string{ + "method": "GET", + }, + ) + nonMatchingLog := makeTestLogEntry( + "test log body", + map[string]string{ + "method": "POST", + }, + ) + + result, err := SimulatePipelinesProcessing( + context.Background(), + testPipelines, + []model.SignozLog{ + matchingLog, + nonMatchingLog, + }, + ) + + require.Nil(err) + require.Equal(2, len(result)) + + // matching log should have been modified as expected. + require.NotEqual( + matchingLog.Attributes_string, + result[0].Attributes_string, + ) + testAttrValue := result[0].Attributes_string["test"] + require.NotNil(testAttrValue) + require.Equal( + testAttrValue, "val", + ) + + require.Equal(result[0].Resources_string, map[string]string{ + "test1": "val1", + "test2": "val2", + }) + + // non-matching log should not be touched. + require.Equal( + nonMatchingLog.Attributes_string, + result[1].Attributes_string, + ) + require.Equal( + nonMatchingLog.Resources_string, + result[1].Resources_string, + ) + +} + +func makeTestLogEntry( + body string, + attributes map[string]string, +) model.SignozLog { + return model.SignozLog{ + Timestamp: uint64(time.Now().UnixNano()), + Body: body, + Attributes_string: attributes, + Resources_string: map[string]string{}, + SeverityText: entry.Info.String(), + SeverityNumber: uint8(entry.Info), + SpanID: uuid.New().String(), + TraceID: uuid.New().String(), + } +} diff --git a/pkg/query-service/app/logs/v3/json_filter.go b/pkg/query-service/app/logs/v3/json_filter.go index 345da5a013..abb0fe6712 100644 --- a/pkg/query-service/app/logs/v3/json_filter.go +++ b/pkg/query-service/app/logs/v3/json_filter.go @@ -52,11 +52,13 @@ var jsonLogOperators = map[v3.FilterOperator]string{ v3.FilterOperatorNotRegex: "NOT match(%s, %s)", v3.FilterOperatorIn: "IN", v3.FilterOperatorNotIn: "NOT IN", + v3.FilterOperatorExists: "JSON_EXISTS(%s, '$.%s')", + v3.FilterOperatorNotExists: "NOT JSON_EXISTS(%s, '$.%s')", v3.FilterOperatorHas: "has(%s, %s)", v3.FilterOperatorNotHas: "NOT has(%s, %s)", } -func getJSONFilterKey(key v3.AttributeKey, isArray bool) (string, error) { +func getJSONFilterKey(key v3.AttributeKey, op v3.FilterOperator, isArray bool) (string, error) { keyArr := strings.Split(key.Key, ".") if len(keyArr) < 2 { return "", fmt.Errorf("incorrect key, should contain at least 2 parts") @@ -67,6 +69,10 @@ func getJSONFilterKey(key v3.AttributeKey, isArray bool) (string, error) { return "", fmt.Errorf("only body can be the root key") } + if op == v3.FilterOperatorExists || op == v3.FilterOperatorNotExists { + return keyArr[0], nil + } + var dataType string var ok bool if dataType, ok = dataTypeMapping[string(key.DataType)]; !ok { @@ -99,29 +105,45 @@ func GetJSONFilter(item v3.FilterItem) (string, error) { dataType = v3.AttributeKeyDataType(val) } - key, err := getJSONFilterKey(item.Key, isArray) + key, err := getJSONFilterKey(item.Key, item.Operator, isArray) if err != nil { return "", err } // non array - value, err := utils.ValidateAndCastValue(item.Value, dataType) - if err != nil { - return "", fmt.Errorf("failed to validate and cast value for %s: %v", item.Key.Key, err) - } - op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator)))) - if logsOp, ok := jsonLogOperators[op]; ok { - switch op { - case v3.FilterOperatorRegex, v3.FilterOperatorNotRegex, v3.FilterOperatorHas, v3.FilterOperatorNotHas: - fmtVal := utils.ClickHouseFormattedValue(value) - return fmt.Sprintf(logsOp, key, fmtVal), nil - case v3.FilterOperatorContains, v3.FilterOperatorNotContains: - return fmt.Sprintf("%s %s '%%%s%%'", key, logsOp, item.Value), nil - default: - fmtVal := utils.ClickHouseFormattedValue(value) - return fmt.Sprintf("%s %s %s", key, logsOp, fmtVal), nil + + var value interface{} + if op != v3.FilterOperatorExists && op != v3.FilterOperatorNotExists { + value, err = utils.ValidateAndCastValue(item.Value, dataType) + if err != nil { + return "", fmt.Errorf("failed to validate and cast value for %s: %v", item.Key.Key, err) } } - return "", fmt.Errorf("unsupported operator: %s", op) + + var filter string + if logsOp, ok := jsonLogOperators[op]; ok { + switch op { + case v3.FilterOperatorExists, v3.FilterOperatorNotExists: + filter = fmt.Sprintf(logsOp, key, strings.Join(strings.Split(item.Key.Key, ".")[1:], ".")) + case v3.FilterOperatorRegex, v3.FilterOperatorNotRegex, v3.FilterOperatorHas, v3.FilterOperatorNotHas: + fmtVal := utils.ClickHouseFormattedValue(value) + filter = fmt.Sprintf(logsOp, key, fmtVal) + case v3.FilterOperatorContains, v3.FilterOperatorNotContains: + filter = fmt.Sprintf("%s %s '%%%s%%'", key, logsOp, item.Value) + default: + fmtVal := utils.ClickHouseFormattedValue(value) + filter = fmt.Sprintf("%s %s %s", key, logsOp, fmtVal) + } + } else { + return "", fmt.Errorf("unsupported operator: %s", op) + } + + // add exists check for non array items as default values of int/float/bool will corrupt the results + if !isArray && !(item.Operator == v3.FilterOperatorExists || item.Operator == v3.FilterOperatorNotExists) { + existsFilter := fmt.Sprintf("JSON_EXISTS(body, '$.%s')", strings.Join(strings.Split(item.Key.Key, ".")[1:], ".")) + filter = fmt.Sprintf("%s AND %s", existsFilter, filter) + } + + return filter, nil } diff --git a/pkg/query-service/app/logs/v3/json_filter_test.go b/pkg/query-service/app/logs/v3/json_filter_test.go index 455d705b1d..2f9a7f22da 100644 --- a/pkg/query-service/app/logs/v3/json_filter_test.go +++ b/pkg/query-service/app/logs/v3/json_filter_test.go @@ -12,6 +12,7 @@ var testGetJSONFilterKeyData = []struct { Key v3.AttributeKey IsArray bool ClickhouseKey string + Operator v3.FilterOperator Error bool }{ { @@ -129,7 +130,7 @@ var testGetJSONFilterKeyData = []struct { func TestGetJSONFilterKey(t *testing.T) { for _, tt := range testGetJSONFilterKeyData { Convey("testgetKey", t, func() { - columnName, err := getJSONFilterKey(tt.Key, tt.IsArray) + columnName, err := getJSONFilterKey(tt.Key, tt.Operator, tt.IsArray) if tt.Error { So(err, ShouldNotBeNil) } else { @@ -209,7 +210,7 @@ var testGetJSONFilterData = []struct { Operator: "=", Value: "hello", }, - Filter: "JSON_VALUE(body, '$.message') = 'hello'", + Filter: "JSON_EXISTS(body, '$.message') AND JSON_VALUE(body, '$.message') = 'hello'", }, { Name: "eq operator number", @@ -222,7 +223,7 @@ var testGetJSONFilterData = []struct { Operator: "=", Value: 1, }, - Filter: "JSONExtract(JSON_VALUE(body, '$.status'), '" + INT64 + "') = 1", + Filter: "JSON_EXISTS(body, '$.status') AND JSONExtract(JSON_VALUE(body, '$.status'), '" + INT64 + "') = 1", }, { Name: "neq operator number", @@ -235,7 +236,7 @@ var testGetJSONFilterData = []struct { Operator: "=", Value: 1.1, }, - Filter: "JSONExtract(JSON_VALUE(body, '$.status'), '" + FLOAT64 + "') = 1.100000", + Filter: "JSON_EXISTS(body, '$.status') AND JSONExtract(JSON_VALUE(body, '$.status'), '" + FLOAT64 + "') = 1.100000", }, { Name: "eq operator bool", @@ -248,7 +249,7 @@ var testGetJSONFilterData = []struct { Operator: "=", Value: true, }, - Filter: "JSONExtract(JSON_VALUE(body, '$.boolkey'), '" + BOOL + "') = true", + Filter: "JSON_EXISTS(body, '$.boolkey') AND JSONExtract(JSON_VALUE(body, '$.boolkey'), '" + BOOL + "') = true", }, { Name: "greater than operator", @@ -261,7 +262,7 @@ var testGetJSONFilterData = []struct { Operator: ">", Value: 1, }, - Filter: "JSONExtract(JSON_VALUE(body, '$.status'), '" + INT64 + "') > 1", + Filter: "JSON_EXISTS(body, '$.status') AND JSONExtract(JSON_VALUE(body, '$.status'), '" + INT64 + "') > 1", }, { Name: "regex operator", @@ -274,7 +275,7 @@ var testGetJSONFilterData = []struct { Operator: "regex", Value: "a*", }, - Filter: "match(JSON_VALUE(body, '$.message'), 'a*')", + Filter: "JSON_EXISTS(body, '$.message') AND match(JSON_VALUE(body, '$.message'), 'a*')", }, { Name: "contains operator", @@ -287,7 +288,20 @@ var testGetJSONFilterData = []struct { Operator: "contains", Value: "a", }, - Filter: "JSON_VALUE(body, '$.message') ILIKE '%a%'", + Filter: "JSON_EXISTS(body, '$.message') AND JSON_VALUE(body, '$.message') ILIKE '%a%'", + }, + { + Name: "exists", + FilterItem: v3.FilterItem{ + Key: v3.AttributeKey{ + Key: "body.message", + DataType: "string", + IsJSON: true, + }, + Operator: "exists", + Value: "", + }, + Filter: "JSON_EXISTS(body, '$.message')", }, } diff --git a/pkg/query-service/app/logs/v3/query_builder.go b/pkg/query-service/app/logs/v3/query_builder.go index 5deb2c26f3..ff989fa61d 100644 --- a/pkg/query-service/app/logs/v3/query_builder.go +++ b/pkg/query-service/app/logs/v3/query_builder.go @@ -280,7 +280,7 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build } if graphLimitQtype == constants.SecondQueryGraphLimit { - filterSubQuery = filterSubQuery + " AND " + fmt.Sprintf("(%s) GLOBAL IN (", getSelectKeys(mq.AggregateOperator, mq.GroupBy)) + "%s)" + filterSubQuery = filterSubQuery + " AND " + fmt.Sprintf("(%s) GLOBAL IN (", getSelectKeys(mq.AggregateOperator, mq.GroupBy)) + "#LIMIT_PLACEHOLDER)" } aggregationKey := "" 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 79f103a0a3..8ae90e2e0f 100644 --- a/pkg/query-service/app/logs/v3/query_builder_test.go +++ b/pkg/query-service/app/logs/v3/query_builder_test.go @@ -908,7 +908,7 @@ var testBuildLogsQueryData = []struct { }, }, TableName: "logs", - ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as name, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND JSON_VALUE(body, '$.message') ILIKE '%a%' AND indexOf(attributes_string_key, 'name') > 0 group by name order by name DESC", + ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as name, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND JSON_EXISTS(body, '$.message') AND JSON_VALUE(body, '$.message') ILIKE '%a%' AND indexOf(attributes_string_key, 'name') > 0 group by name order by name DESC", }, { Name: "TABLE: Test count with JSON Filter Array, groupBy, orderBy", @@ -1193,7 +1193,7 @@ var testPrepLogsQueryData = []struct { Limit: 2, }, TableName: "logs", - ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 AND has(attributes_string_key, 'name') AND (method) GLOBAL IN (%s) group by method,ts order by value DESC", + ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 AND has(attributes_string_key, 'name') AND (method) GLOBAL IN (#LIMIT_PLACEHOLDER) group by method,ts order by value DESC", Options: Options{GraphLimitQtype: constants.SecondQueryGraphLimit}, }, { @@ -1216,7 +1216,7 @@ var testPrepLogsQueryData = []struct { Limit: 2, }, TableName: "logs", - ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 AND has(attributes_string_key, 'name') AND (method) GLOBAL IN (%s) group by method,ts order by method ASC", + ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 AND has(attributes_string_key, 'name') AND (method) GLOBAL IN (#LIMIT_PLACEHOLDER) group by method,ts order by method ASC", Options: Options{GraphLimitQtype: constants.SecondQueryGraphLimit}, }, // Live tail diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index a957ef85e7..4e6350b70e 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -734,6 +734,14 @@ func parseSetApdexScoreRequest(r *http.Request) (*model.ApdexSettings, error) { return &req, nil } +func parseInsertIngestionKeyRequest(r *http.Request) (*model.IngestionKey, error) { + var req model.IngestionKey + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return &req, nil +} + func parseRegisterRequest(r *http.Request) (*auth.RegisterRequest, error) { var req auth.RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/pkg/query-service/app/querier/helper.go b/pkg/query-service/app/querier/helper.go index 4d809a18f3..82bf2cc464 100644 --- a/pkg/query-service/app/querier/helper.go +++ b/pkg/query-service/app/querier/helper.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "sync" "time" @@ -64,7 +65,7 @@ func (q *querier) runBuilderQuery( ch <- channelResult{Err: err, Name: queryName, Query: placeholderQuery, Series: nil} return } - query = fmt.Sprintf(placeholderQuery, limitQuery) + query = strings.Replace(placeholderQuery, "#LIMIT_PLACEHOLDER", limitQuery, 1) } else { query, err = logsV3.PrepareLogsQuery( params.Start, diff --git a/pkg/query-service/collectorsimulator/collectorsimulator.go b/pkg/query-service/collectorsimulator/collectorsimulator.go new file mode 100644 index 0000000000..c4537cf3ee --- /dev/null +++ b/pkg/query-service/collectorsimulator/collectorsimulator.go @@ -0,0 +1,234 @@ +package collectorsimulator + +import ( + "bytes" + "context" + "fmt" + "strings" + + "github.com/google/uuid" + "github.com/knadh/koanf/parsers/yaml" + "github.com/pkg/errors" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/confmap/provider/yamlprovider" + "go.opentelemetry.io/collector/connector" + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/otelcol" + "go.opentelemetry.io/collector/processor" + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/service" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "go.signoz.io/signoz/pkg/query-service/collectorsimulator/inmemoryexporter" + "go.signoz.io/signoz/pkg/query-service/collectorsimulator/inmemoryreceiver" + "go.signoz.io/signoz/pkg/query-service/model" +) + +// Puts together a collector service with inmemory receiver and exporter +// for simulating processing of signal data through an otel collector +type CollectorSimulator struct { + // collector service to be used for the simulation + collectorSvc *service.Service + + // Buffer where collectorSvc will log errors. + collectorErrorLogsBuffer *bytes.Buffer + + // error channel where collector components will report fatal errors + // Gets passed in as AsyncErrorChannel in service.Settings when creating a collector service. + collectorErrorChannel chan error + + // Unique ids of inmemory receiver and exporter instances that + // will be created by collectorSvc + inMemoryReceiverId string + inMemoryExporterId string +} + +func NewCollectorSimulator( + ctx context.Context, + signalType component.DataType, + processorFactories map[component.Type]processor.Factory, + processorConfigs []ProcessorConfig, +) (*CollectorSimulator, *model.ApiError) { + // Put together collector component factories for use in the simulation + receiverFactories, err := receiver.MakeFactoryMap(inmemoryreceiver.NewFactory()) + if err != nil { + return nil, model.InternalError(errors.Wrap(err, "could not create receiver factories.")) + } + exporterFactories, err := exporter.MakeFactoryMap(inmemoryexporter.NewFactory()) + if err != nil { + return nil, model.InternalError(errors.Wrap(err, "could not create processor factories.")) + } + factories := otelcol.Factories{ + Receivers: receiverFactories, + Processors: processorFactories, + Exporters: exporterFactories, + } + + // Prepare collector config yaml for simulation + inMemoryReceiverId := uuid.NewString() + inMemoryExporterId := uuid.NewString() + + collectorConfYaml, err := generateSimulationConfig( + signalType, inMemoryReceiverId, processorConfigs, inMemoryExporterId, + ) + if err != nil { + return nil, model.BadRequest(errors.Wrap(err, "could not generate collector config")) + } + + // Parse and validate collector config + yamlP := yamlprovider.New() + confProvider, err := otelcol.NewConfigProvider(otelcol.ConfigProviderSettings{ + ResolverSettings: confmap.ResolverSettings{ + URIs: []string{"yaml:" + string(collectorConfYaml)}, + Providers: map[string]confmap.Provider{yamlP.Scheme(): yamlP}, + }, + }) + if err != nil { + return nil, model.BadRequest(errors.Wrap(err, "could not create config provider.")) + } + collectorCfg, err := confProvider.Get(ctx, factories) + if err != nil { + return nil, model.BadRequest(errors.Wrap(err, "failed to parse collector config")) + } + + if err = collectorCfg.Validate(); err != nil { + return nil, model.BadRequest(errors.Wrap(err, "invalid collector config")) + } + + // Build and start collector service. + collectorErrChan := make(chan error) + var collectorErrBuf bytes.Buffer + svcSettings := service.Settings{ + Receivers: receiver.NewBuilder(collectorCfg.Receivers, factories.Receivers), + Processors: processor.NewBuilder(collectorCfg.Processors, factories.Processors), + Exporters: exporter.NewBuilder(collectorCfg.Exporters, factories.Exporters), + Connectors: connector.NewBuilder(collectorCfg.Connectors, factories.Connectors), + Extensions: extension.NewBuilder(collectorCfg.Extensions, factories.Extensions), + AsyncErrorChannel: collectorErrChan, + LoggingOptions: []zap.Option{ + zap.ErrorOutput(zapcore.AddSync(&collectorErrBuf)), + }, + } + + collectorSvc, err := service.New(ctx, svcSettings, collectorCfg.Service) + if err != nil { + return nil, model.InternalError(errors.Wrap(err, "could not instantiate collector service")) + } + + return &CollectorSimulator{ + inMemoryReceiverId: inMemoryReceiverId, + inMemoryExporterId: inMemoryExporterId, + collectorSvc: collectorSvc, + collectorErrorLogsBuffer: &collectorErrBuf, + collectorErrorChannel: collectorErrChan, + }, nil +} + +func (l *CollectorSimulator) Start(ctx context.Context) ( + func(), *model.ApiError, +) { + // Calling collectorSvc.Start below will in turn call Start on + // inmemory receiver and exporter instances created by collectorSvc + // + // inmemory components are indexed in a global map after Start is called + // on them and will have to be cleaned up to ensure there is no memory leak + cleanupFn := func() { + inmemoryreceiver.CleanupInstance(l.inMemoryReceiverId) + inmemoryexporter.CleanupInstance(l.inMemoryExporterId) + } + + err := l.collectorSvc.Start(ctx) + if err != nil { + return cleanupFn, model.InternalError(errors.Wrap(err, "could not start collector service for simulation")) + } + + return cleanupFn, nil +} + +func (l *CollectorSimulator) GetReceiver() *inmemoryreceiver.InMemoryReceiver { + return inmemoryreceiver.GetReceiverInstance(l.inMemoryReceiverId) +} + +func (l *CollectorSimulator) GetExporter() *inmemoryexporter.InMemoryExporter { + return inmemoryexporter.GetExporterInstance(l.inMemoryExporterId) +} + +func (l *CollectorSimulator) Shutdown(ctx context.Context) ( + simulationErrs []string, apiErr *model.ApiError, +) { + shutdownErr := l.collectorSvc.Shutdown(ctx) + + // Collect all errors logged or reported by collectorSvc + simulationErrs = []string{} + close(l.collectorErrorChannel) + for reportedErr := range l.collectorErrorChannel { + simulationErrs = append(simulationErrs, reportedErr.Error()) + } + + if l.collectorErrorLogsBuffer.Len() > 0 { + errBufText := strings.TrimSpace(l.collectorErrorLogsBuffer.String()) + errBufLines := strings.Split(errBufText, "\n") + simulationErrs = append(simulationErrs, errBufLines...) + } + + if shutdownErr != nil { + return simulationErrs, model.InternalError(errors.Wrap( + shutdownErr, "could not shutdown the collector service", + )) + } + return simulationErrs, nil +} + +func generateSimulationConfig( + signalType component.DataType, + receiverId string, + processorConfigs []ProcessorConfig, + exporterId string, +) ([]byte, error) { + baseConf := fmt.Sprintf(` + receivers: + memory: + id: %s + exporters: + memory: + id: %s + service: + telemetry: + metrics: + level: none + logs: + level: error + `, receiverId, exporterId) + + simulationConf, err := yaml.Parser().Unmarshal([]byte(baseConf)) + if err != nil { + return nil, err + } + + processors := map[string]interface{}{} + procNamesInOrder := []string{} + for _, processorConf := range processorConfigs { + processors[processorConf.Name] = processorConf.Config + procNamesInOrder = append(procNamesInOrder, processorConf.Name) + } + simulationConf["processors"] = processors + + svc := simulationConf["service"].(map[string]interface{}) + svc["pipelines"] = map[string]interface{}{ + string(signalType): map[string]interface{}{ + "receivers": []string{"memory"}, + "processors": procNamesInOrder, + "exporters": []string{"memory"}, + }, + } + + simulationConfYaml, err := yaml.Parser().Marshal(simulationConf) + if err != nil { + return nil, err + } + + return simulationConfYaml, nil +} diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/config.go b/pkg/query-service/collectorsimulator/inmemoryexporter/config.go new file mode 100644 index 0000000000..5b23b041ce --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/config.go @@ -0,0 +1,16 @@ +package inmemoryexporter + +import "fmt" + +type Config struct { + // Unique id for the exporter. + // Useful for getting a hold of the exporter in code that doesn't control its instantiation. + Id string `mapstructure:"id"` +} + +func (c *Config) Validate() error { + if len(c.Id) < 1 { + return fmt.Errorf("inmemory exporter: id is required") + } + return nil +} diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go b/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go new file mode 100644 index 0000000000..29749757dc --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go @@ -0,0 +1,48 @@ +package inmemoryexporter + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/confmap" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + rawConf *confmap.Conf + errorExpected bool + }{ + { + name: "with id", + rawConf: confmap.NewFromStringMap(map[string]interface{}{ + "id": "test_exporter", + }), + errorExpected: false, + }, + { + name: "empty id", + rawConf: confmap.NewFromStringMap(map[string]interface{}{ + "id": "", + }), + errorExpected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + err := component.UnmarshalConfig(tt.rawConf, cfg) + require.NoError(t, err, "could not UnmarshalConfig") + + err = component.ValidateConfig(cfg) + if tt.errorExpected { + require.NotNilf(t, err, "Invalid config did not return validation error: %v", cfg) + } else { + require.NoErrorf(t, err, "Valid config returned validation error: %v", cfg) + } + }) + } +} diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/exporter.go b/pkg/query-service/collectorsimulator/inmemoryexporter/exporter.go new file mode 100644 index 0000000000..3cff186016 --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/exporter.go @@ -0,0 +1,86 @@ +package inmemoryexporter + +import ( + "context" + "fmt" + "sync" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/plog" +) + +// An in-memory exporter for testing and generating previews. +type InMemoryExporter struct { + // Unique identifier for the exporter. + id string + // mu protects the data below + mu sync.Mutex + // slice of pdata.Logs that were received by this exporter. + logs []plog.Logs +} + +// ConsumeLogs implements component.LogsExporter. +func (e *InMemoryExporter) ConsumeLogs(ctx context.Context, ld plog.Logs) error { + e.mu.Lock() + defer e.mu.Unlock() + + e.logs = append(e.logs, ld) + return nil +} + +func (e *InMemoryExporter) GetLogs() []plog.Logs { + e.mu.Lock() + defer e.mu.Unlock() + + return e.logs +} + +func (e *InMemoryExporter) ResetLogs() { + e.mu.Lock() + defer e.mu.Unlock() + + e.logs = nil +} + +func (e *InMemoryExporter) Capabilities() consumer.Capabilities { + return consumer.Capabilities{MutatesData: false} +} + +// Keep track of all exporter instances in the process. +// Useful for getting a hold of the exporter in scenarios where one doesn't +// create the instances. Eg: bringing up a collector service from collector config +var allExporterInstances map[string]*InMemoryExporter +var allExportersLock sync.Mutex + +func init() { + allExporterInstances = make(map[string]*InMemoryExporter) +} + +func GetExporterInstance(id string) *InMemoryExporter { + return allExporterInstances[id] +} + +func CleanupInstance(exporterId string) { + allExportersLock.Lock() + defer allExportersLock.Unlock() + + delete(allExporterInstances, exporterId) +} + +func (e *InMemoryExporter) Start(ctx context.Context, host component.Host) error { + allExportersLock.Lock() + defer allExportersLock.Unlock() + + if allExporterInstances[e.id] != nil { + return fmt.Errorf("exporter with id %s is already running", e.id) + } + + allExporterInstances[e.id] = e + return nil +} + +func (e *InMemoryExporter) Shutdown(ctx context.Context) error { + CleanupInstance(e.id) + return nil +} diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go b/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go new file mode 100644 index 0000000000..4fe4753d72 --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go @@ -0,0 +1,67 @@ +package inmemoryexporter + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/exporter" +) + +func TestExporterLifecycle(t *testing.T) { + require := require.New(t) + testExporterId := uuid.NewString() + + // Should be able to get a hold of the exporter after starting it. + require.Nil(GetExporterInstance(testExporterId)) + + constructed, err := makeTestExporter(testExporterId) + require.Nil(err, "could not make test exporter") + + err = constructed.Start(context.Background(), componenttest.NewNopHost()) + require.Nil(err, "could not start test exporter") + + testExporter := GetExporterInstance(testExporterId) + require.NotNil(testExporter, "could not get exporter instance by Id") + + // Should not be able to start 2 exporters with the same id + constructed2, err := makeTestExporter(testExporterId) + require.Nil(err, "could not create second exporter with same id") + + err = constructed2.Start(context.Background(), componenttest.NewNopHost()) + require.NotNil(err, "should not be able to start another exporter with same id before shutting down the previous one") + + // Should not be able to get a hold of an exporter after shutdown + testExporter.Shutdown(context.Background()) + require.Nil(GetExporterInstance(testExporterId), "should not be able to find exporter instance after shutdown") + + // Should be able to start a new exporter with same id after shutting down + constructed3, err := makeTestExporter(testExporterId) + require.Nil(err, "could not make exporter with same Id after shutting down previous one") + + err = constructed3.Start(context.Background(), componenttest.NewNopHost()) + require.Nil(err, "should be able to start another exporter with same id after shutting down the previous one") + + testExporter3 := GetExporterInstance(testExporterId) + require.NotNil(testExporter3, "could not get exporter instance by Id") + + testExporter3.Shutdown(context.Background()) + require.Nil(GetExporterInstance(testExporterId)) +} + +func makeTestExporter(exporterId string) (exporter.Logs, error) { + factory := NewFactory() + + cfg := factory.CreateDefaultConfig() + component.UnmarshalConfig(confmap.NewFromStringMap( + map[string]interface{}{"id": exporterId}), cfg, + ) + + return factory.CreateLogsExporter( + context.Background(), exporter.CreateSettings{}, cfg, + ) +} diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/factory.go b/pkg/query-service/collectorsimulator/inmemoryexporter/factory.go new file mode 100644 index 0000000000..7752693060 --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/factory.go @@ -0,0 +1,34 @@ +package inmemoryexporter + +import ( + "context" + + "github.com/google/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/exporter" +) + +func createDefaultConfig() component.Config { + return &Config{ + Id: uuid.NewString(), + } +} + +func createLogsExporter( + _ context.Context, _ exporter.CreateSettings, config component.Config, +) (exporter.Logs, error) { + if err := component.ValidateConfig(config); err != nil { + return nil, errors.Wrap(err, "invalid inmemory exporter config") + } + return &InMemoryExporter{ + id: config.(*Config).Id, + }, nil +} + +func NewFactory() exporter.Factory { + return exporter.NewFactory( + "memory", + createDefaultConfig, + exporter.WithLogs(createLogsExporter, component.StabilityLevelBeta)) +} diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/factory_test.go b/pkg/query-service/collectorsimulator/inmemoryexporter/factory_test.go new file mode 100644 index 0000000000..1a9481169a --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/factory_test.go @@ -0,0 +1,28 @@ +package inmemoryexporter + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/exporter" +) + +func TestCreateDefaultConfig(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + assert.NotNil(t, cfg, "failed to create default config") + assert.NoError(t, componenttest.CheckConfigStruct(cfg)) +} + +func TestCreateLogsExporter(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + + te, err := factory.CreateLogsExporter( + context.Background(), exporter.CreateSettings{}, cfg, + ) + assert.NoError(t, err) + assert.NotNil(t, te) +} diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/config.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/config.go new file mode 100644 index 0000000000..6df842ce3e --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/config.go @@ -0,0 +1,16 @@ +package inmemoryreceiver + +import "fmt" + +type Config struct { + // Unique id for the receiver. + // Useful for getting a hold of the receiver in code that doesn't control its instantiation. + Id string `mapstructure:"id"` +} + +func (c *Config) Validate() error { + if len(c.Id) < 1 { + return fmt.Errorf("inmemory receiver: id is required") + } + return nil +} diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go new file mode 100644 index 0000000000..a0daf71c45 --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go @@ -0,0 +1,48 @@ +package inmemoryreceiver + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/confmap" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + rawConf *confmap.Conf + errorExpected bool + }{ + { + name: "with id", + rawConf: confmap.NewFromStringMap(map[string]interface{}{ + "id": "test_receiver", + }), + errorExpected: false, + }, + { + name: "empty id", + rawConf: confmap.NewFromStringMap(map[string]interface{}{ + "id": "", + }), + errorExpected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + err := component.UnmarshalConfig(tt.rawConf, cfg) + require.NoError(t, err, "could not UnmarshalConfig") + + err = component.ValidateConfig(cfg) + if tt.errorExpected { + require.NotNilf(t, err, "Invalid config did not return validation error: %v", cfg) + } else { + require.NoErrorf(t, err, "Valid config returned validation error: %v", cfg) + } + }) + } +} diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/factory.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/factory.go new file mode 100644 index 0000000000..9db222cc43 --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/factory.go @@ -0,0 +1,41 @@ +package inmemoryreceiver + +import ( + "context" + + "github.com/google/uuid" + "github.com/pkg/errors" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/receiver" +) + +func createDefaultConfig() component.Config { + return &Config{ + Id: uuid.NewString(), + } +} + +func createLogsReceiver( + _ context.Context, + _ receiver.CreateSettings, + config component.Config, + consumer consumer.Logs, +) (receiver.Logs, error) { + if err := component.ValidateConfig(config); err != nil { + return nil, errors.Wrap(err, "invalid inmemory receiver config") + } + return &InMemoryReceiver{ + id: config.(*Config).Id, + nextConsumer: consumer, + }, nil + +} + +// NewFactory creates a new OTLP receiver factory. +func NewFactory() receiver.Factory { + return receiver.NewFactory( + "memory", + createDefaultConfig, + receiver.WithLogs(createLogsReceiver, component.StabilityLevelBeta)) +} diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/factory_test.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/factory_test.go new file mode 100644 index 0000000000..7bdcd80bee --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/factory_test.go @@ -0,0 +1,29 @@ +package inmemoryreceiver + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver" +) + +func TestCreateDefaultConfig(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + assert.NotNil(t, cfg, "failed to create default config") + assert.NoError(t, componenttest.CheckConfigStruct(cfg)) +} + +func TestCreateLogsReceiver(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + + te, err := factory.CreateLogsReceiver( + context.Background(), receiver.CreateSettings{}, cfg, consumertest.NewNop(), + ) + assert.NoError(t, err) + assert.NotNil(t, te) +} diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver.go new file mode 100644 index 0000000000..d4b0a2abfe --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver.go @@ -0,0 +1,64 @@ +package inmemoryreceiver + +import ( + "context" + "fmt" + "sync" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/plog" +) + +// In memory receiver for testing and simulation +type InMemoryReceiver struct { + // Unique identifier for the receiver. + id string + + nextConsumer consumer.Logs +} + +func (r *InMemoryReceiver) ConsumeLogs(ctx context.Context, ld plog.Logs) error { + return r.nextConsumer.ConsumeLogs(ctx, ld) +} + +func (r *InMemoryReceiver) Capabilities() consumer.Capabilities { + return consumer.Capabilities{MutatesData: false} +} + +// Keep track of all receiver instances in the process. +// Useful for getting a hold of the receiver in scenarios where one doesn't +// create the instances. Eg: bringing up a collector service from collector config +var allReceiverInstances map[string]*InMemoryReceiver +var allReceiversLock sync.Mutex + +func init() { + allReceiverInstances = make(map[string]*InMemoryReceiver) +} + +func CleanupInstance(receiverId string) { + allReceiversLock.Lock() + defer allReceiversLock.Unlock() + delete(allReceiverInstances, receiverId) +} + +func (r *InMemoryReceiver) Start(ctx context.Context, host component.Host) error { + allReceiversLock.Lock() + defer allReceiversLock.Unlock() + + if allReceiverInstances[r.id] != nil { + return fmt.Errorf("receiver with id %s is already running", r.id) + } + + allReceiverInstances[r.id] = r + return nil +} + +func (r *InMemoryReceiver) Shutdown(ctx context.Context) error { + CleanupInstance(r.id) + return nil +} + +func GetReceiverInstance(id string) *InMemoryReceiver { + return allReceiverInstances[id] +} diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go new file mode 100644 index 0000000000..4fe7169cc7 --- /dev/null +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go @@ -0,0 +1,68 @@ +package inmemoryreceiver + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver" +) + +func TestReceiverLifecycle(t *testing.T) { + require := require.New(t) + testReceiverId := uuid.NewString() + + // Should be able to get a hold of the receiver after starting it. + require.Nil(GetReceiverInstance(testReceiverId), "receiver instance should not exist before Start()") + + constructed, err := makeTestLogReceiver(testReceiverId) + require.Nil(err, "could not make test receiver") + + err = constructed.Start(context.Background(), componenttest.NewNopHost()) + require.Nil(err, "could not start test receiver") + + testReceiver := GetReceiverInstance(testReceiverId) + require.NotNil(testReceiver, "could not get receiver instance by Id") + + // Should not be able to start 2 receivers with the same id + constructed2, err := makeTestLogReceiver(testReceiverId) + require.Nil(err, "could not create second receiver with same id") + + err = constructed2.Start(context.Background(), componenttest.NewNopHost()) + require.NotNil(err, "should not be able to start another receiver with same id before shutting down the previous one") + + // Should not be able to get a hold of an receiver after shutdown + testReceiver.Shutdown(context.Background()) + require.Nil(GetReceiverInstance(testReceiverId), "should not be able to find inmemory receiver after shutdown") + + // Should be able to start a new receiver with same id after shutting down + constructed3, err := makeTestLogReceiver(testReceiverId) + require.Nil(err, "could not make receiver with same Id after shutting down old one") + + err = constructed3.Start(context.Background(), componenttest.NewNopHost()) + require.Nil(err, "should be able to start another receiver with same id after shutting down the previous one") + + testReceiver3 := GetReceiverInstance(testReceiverId) + require.NotNil(testReceiver3, "could not get receiver instance by Id") + + testReceiver3.Shutdown(context.Background()) + require.Nil(GetReceiverInstance(testReceiverId)) +} + +func makeTestLogReceiver(receiverId string) (receiver.Logs, error) { + factory := NewFactory() + + cfg := factory.CreateDefaultConfig() + component.UnmarshalConfig(confmap.NewFromStringMap( + map[string]interface{}{"id": receiverId}), cfg, + ) + + return factory.CreateLogsReceiver( + context.Background(), receiver.CreateSettings{}, cfg, consumertest.NewNop(), + ) +} diff --git a/pkg/query-service/collectorsimulator/logs.go b/pkg/query-service/collectorsimulator/logs.go new file mode 100644 index 0000000000..ab445f79eb --- /dev/null +++ b/pkg/query-service/collectorsimulator/logs.go @@ -0,0 +1,122 @@ +package collectorsimulator + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/processor" + "go.signoz.io/signoz/pkg/query-service/model" +) + +type ProcessorConfig struct { + Name string + Config map[string]interface{} +} + +// Simulate processing of logs through the otel collector. +// Useful for testing, validation and generating previews. +func SimulateLogsProcessing( + ctx context.Context, + processorFactories map[component.Type]processor.Factory, + processorConfigs []ProcessorConfig, + logs []plog.Logs, + timeout time.Duration, +) ( + outputLogs []plog.Logs, collectorErrs []string, apiErr *model.ApiError, +) { + // Construct and start a simulator (wraps a collector service) + simulator, apiErr := NewCollectorSimulator( + ctx, component.DataTypeLogs, processorFactories, processorConfigs, + ) + if apiErr != nil { + return nil, nil, model.WrapApiError(apiErr, "could not create logs processing simulator") + } + + simulatorCleanup, apiErr := simulator.Start(ctx) + // We can not rely on collector service to shutdown successfully and cleanup refs to inmemory components. + defer simulatorCleanup() + if apiErr != nil { + return nil, nil, apiErr + } + + // Do the simulation + for _, plog := range logs { + apiErr = SendLogsToSimulator(ctx, simulator, plog) + if apiErr != nil { + return nil, nil, model.WrapApiError(apiErr, "could not consume logs for simulation") + } + } + + result, apiErr := GetProcessedLogsFromSimulator( + simulator, len(logs), timeout, + ) + if apiErr != nil { + return nil, nil, model.InternalError(model.WrapApiError(apiErr, + "could not get processed logs from simulator", + )) + } + + // Shut down the simulator + simulationErrs, apiErr := simulator.Shutdown(ctx) + if apiErr != nil { + return nil, simulationErrs, model.WrapApiError(apiErr, + "could not shutdown logs processing simulator", + ) + } + + return result, simulationErrs, nil +} + +func SendLogsToSimulator( + ctx context.Context, + simulator *CollectorSimulator, + plog plog.Logs, +) *model.ApiError { + receiver := simulator.GetReceiver() + if receiver == nil { + return model.InternalError(fmt.Errorf("could not find in memory receiver for simulator")) + } + if err := receiver.ConsumeLogs(ctx, plog); err != nil { + return model.InternalError(errors.Wrap(err, + "inmemory receiver could not consume logs for simulation", + )) + } + return nil +} + +func GetProcessedLogsFromSimulator( + simulator *CollectorSimulator, + minLogCount int, + timeout time.Duration, +) ( + []plog.Logs, *model.ApiError, +) { + exporter := simulator.GetExporter() + if exporter == nil { + return nil, model.InternalError(fmt.Errorf("could not find in memory exporter for simulator")) + } + + // Must do a time based wait to ensure all logs come through. + // For example, logstransformprocessor does internal batching and it + // takes (processorCount * batchTime) for logs to get through. + startTsMillis := time.Now().UnixMilli() + for { + elapsedMillis := time.Now().UnixMilli() - startTsMillis + if elapsedMillis > timeout.Milliseconds() { + break + } + + exportedLogs := exporter.GetLogs() + if len(exportedLogs) >= minLogCount { + return exportedLogs, nil + } + + time.Sleep(50 * time.Millisecond) + } + + return exporter.GetLogs(), nil +} diff --git a/pkg/query-service/collectorsimulator/logs_test.go b/pkg/query-service/collectorsimulator/logs_test.go new file mode 100644 index 0000000000..796d19f00f --- /dev/null +++ b/pkg/query-service/collectorsimulator/logs_test.go @@ -0,0 +1,113 @@ +package collectorsimulator + +import ( + "context" + "testing" + "time" + + "github.com/knadh/koanf/parsers/yaml" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/logstransformprocessor" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/processor" +) + +func TestLogsProcessingSimulation(t *testing.T) { + require := require.New(t) + + inputLogs := []plog.Logs{ + makeTestPlog("test log 1", map[string]string{ + "method": "GET", + }), + makeTestPlog("test log 2", map[string]string{ + "method": "POST", + }), + } + + testLogstransformConf1, err := yaml.Parser().Unmarshal([]byte(` + operators: + - type: router + id: router_signoz + routes: + - output: add + expr: attributes.method == "GET" + default: noop + - type: add + id: add + field: attributes.test + value: test-value-get + - type: noop + id: noop + `)) + require.Nil(err, "could not unmarshal test logstransform op config") + testProcessor1 := ProcessorConfig{ + Name: "logstransform/test", + Config: testLogstransformConf1, + } + + testLogstransformConf2, err := yaml.Parser().Unmarshal([]byte(` + operators: + - type: router + id: router_signoz + routes: + - output: add + expr: attributes.method == "POST" + default: noop + - type: add + id: add + field: attributes.test + value: test-value-post + - type: noop + id: noop + `)) + require.Nil(err, "could not unmarshal test logstransform op config") + testProcessor2 := ProcessorConfig{ + Name: "logstransform/test2", + Config: testLogstransformConf2, + } + + processorFactories, err := processor.MakeFactoryMap( + logstransformprocessor.NewFactory(), + ) + require.Nil(err, "could not create processors factory map") + + outputLogs, collectorErrs, apiErr := SimulateLogsProcessing( + context.Background(), + processorFactories, + []ProcessorConfig{testProcessor1, testProcessor2}, + inputLogs, + 300*time.Millisecond, + ) + require.Nil(apiErr, apiErr.ToError().Error()) + require.Equal(len(collectorErrs), 0) + + for _, l := range outputLogs { + rl := l.ResourceLogs().At(0) + sl := rl.ScopeLogs().At(0) + record := sl.LogRecords().At(0) + method, exists := record.Attributes().Get("method") + require.True(exists) + testVal, exists := record.Attributes().Get("test") + require.True(exists) + if method.Str() == "GET" { + require.Equal(testVal.Str(), "test-value-get") + } else { + require.Equal(testVal.Str(), "test-value-post") + } + } +} + +func makeTestPlog(body string, attrsStr map[string]string) plog.Logs { + pl := plog.NewLogs() + rl := pl.ResourceLogs().AppendEmpty() + + scopeLog := rl.ScopeLogs().AppendEmpty() + slRecord := scopeLog.LogRecords().AppendEmpty() + slRecord.Body().SetStr(body) + slAttribs := slRecord.Attributes() + for k, v := range attrsStr { + slAttribs.PutStr(k, v) + } + + return pl +} diff --git a/pkg/query-service/dao/interface.go b/pkg/query-service/dao/interface.go index 068c8d167d..9f99d67c74 100644 --- a/pkg/query-service/dao/interface.go +++ b/pkg/query-service/dao/interface.go @@ -35,6 +35,8 @@ type Queries interface { GetApdexSettings(ctx context.Context, services []string) ([]model.ApdexSettings, *model.ApiError) + GetIngestionKeys(ctx context.Context) ([]model.IngestionKey, *model.ApiError) + PrecheckLogin(ctx context.Context, email, sourceUrl string) (*model.PrecheckResponse, model.BaseApiError) } @@ -62,4 +64,6 @@ type Mutations interface { UpdateUserGroup(ctx context.Context, userId, groupId string) *model.ApiError SetApdexSettings(ctx context.Context, set *model.ApdexSettings) *model.ApiError + + InsertIngestionKey(ctx context.Context, ingestionKey *model.IngestionKey) *model.ApiError } diff --git a/pkg/query-service/dao/sqlite/connection.go b/pkg/query-service/dao/sqlite/connection.go index dd113a2863..f79d67a122 100644 --- a/pkg/query-service/dao/sqlite/connection.go +++ b/pkg/query-service/dao/sqlite/connection.go @@ -78,6 +78,14 @@ func InitDB(dataSourceName string) (*ModelDaoSqlite, error) { threshold FLOAT NOT NULL, exclude_status_codes TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS ingestion_keys ( + key_id TEXT PRIMARY KEY, + name TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + ingestion_key TEXT NOT NULL, + ingestion_url TEXT NOT NULL, + data_region TEXT NOT NULL + ); ` _, err = db.Exec(table_schema) diff --git a/pkg/query-service/dao/sqlite/ingestion.go b/pkg/query-service/dao/sqlite/ingestion.go new file mode 100644 index 0000000000..79d021e176 --- /dev/null +++ b/pkg/query-service/dao/sqlite/ingestion.go @@ -0,0 +1,39 @@ +package sqlite + +import ( + "context" + + "go.signoz.io/signoz/pkg/query-service/model" +) + +func (mds *ModelDaoSqlite) GetIngestionKeys(ctx context.Context) ([]model.IngestionKey, *model.ApiError) { + ingestion_keys := []model.IngestionKey{} + err := mds.db.Select(&ingestion_keys, `SELECT * FROM ingestion_keys`) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return ingestion_keys, nil +} + +func (mds *ModelDaoSqlite) InsertIngestionKey(ctx context.Context, ingestion_key *model.IngestionKey) *model.ApiError { + _, err := mds.db.ExecContext(ctx, ` + INSERT INTO ingestion_keys ( + ingestion_key, + name, + key_id, + ingestion_url, + data_region + ) VALUES ( + ?, + ?, + ?, + ?, + ? + )`, ingestion_key.IngestionKey, ingestion_key.Name, ingestion_key.KeyId, ingestion_key.IngestionURL, ingestion_key.DataRegion) + if err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + return nil +} diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index 15cc4868b3..b25888f607 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -81,7 +81,7 @@ type Reader interface { // Logs GetLogFields(ctx context.Context) (*model.GetFieldsResponse, *model.ApiError) UpdateLogField(ctx context.Context, field *model.UpdateField) *model.ApiError - GetLogs(ctx context.Context, params *model.LogsFilterParams) (*[]model.GetLogsResponse, *model.ApiError) + GetLogs(ctx context.Context, params *model.LogsFilterParams) (*[]model.SignozLog, *model.ApiError) TailLogs(ctx context.Context, client *model.LogsTailClient) AggregateLogs(ctx context.Context, params *model.LogsAggregateParams) (*model.GetLogsAggregatesResponse, *model.ApiError) GetLogAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) diff --git a/pkg/query-service/model/db.go b/pkg/query-service/model/db.go index f1d7817fc7..2968bf6606 100644 --- a/pkg/query-service/model/db.go +++ b/pkg/query-service/model/db.go @@ -4,6 +4,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "time" ) type Organization struct { @@ -42,6 +43,15 @@ type ApdexSettings struct { ExcludeStatusCodes string `json:"excludeStatusCodes" db:"exclude_status_codes"` // sqlite doesn't support array type } +type IngestionKey struct { + KeyId string `json:"keyId" db:"key_id"` + Name string `json:"name" db:"name"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + IngestionKey string `json:"ingestionKey" db:"ingestion_key"` + IngestionURL string `json:"ingestionURL" db:"ingestion_url"` + DataRegion string `json:"dataRegion" db:"data_region"` +} + type UserFlag map[string]string func (uf UserFlag) Value() (driver.Value, error) { diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index 3ca7126636..c80f223163 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -538,7 +538,8 @@ type GetFieldsResponse struct { Interesting []LogField `json:"interesting"` } -type GetLogsResponse struct { +// Represents a log record in query service requests and responses. +type SignozLog struct { Timestamp uint64 `json:"timestamp" ch:"timestamp"` ID string `json:"id" ch:"id"` TraceID string `json:"trace_id" ch:"trace_id"` @@ -555,7 +556,7 @@ type GetLogsResponse struct { type LogsTailClient struct { Name string - Logs chan *GetLogsResponse + Logs chan *SignozLog Done chan *bool Error chan error Filter LogsFilterParams diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index bd9811fdaa..09851a423d 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -625,7 +625,7 @@ type Result struct { type LogsLiveTailClient struct { Name string - Logs chan *model.GetLogsResponse + Logs chan *model.SignozLog Done chan *bool Error chan error } diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index cbdb0f48f1..6d7bc5160b 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -190,7 +190,7 @@ services: <<: *clickhouse-depends otel-collector: - image: signoz/signoz-otel-collector:0.79.7 + image: signoz/signoz-otel-collector:0.79.8 container_name: signoz-otel-collector command: [ @@ -221,7 +221,7 @@ services: <<: *clickhouse-depends otel-collector-metrics: - image: signoz/signoz-otel-collector:0.79.7 + image: signoz/signoz-otel-collector:0.79.8 container_name: signoz-otel-collector-metrics command: [