diff --git a/.github/workflows/e2e-k3s.yaml b/.github/workflows/e2e-k3s.yaml index 6b14a9c975..da3db01917 100644 --- a/.github/workflows/e2e-k3s.yaml +++ b/.github/workflows/e2e-k3s.yaml @@ -16,6 +16,8 @@ jobs: uses: actions/checkout@v2 - name: Build query-service image + env: + DEV_BUILD: 1 run: make build-ee-query-service-amd64 - name: Build frontend image diff --git a/Makefile b/Makefile index 17a4b32fb6..c46effa9a9 100644 --- a/Makefile +++ b/Makefile @@ -120,14 +120,10 @@ down-local: down -v run-x86: - @docker-compose -f \ - $(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \ - up --build -d + @docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up --build -d down-x86: - @docker-compose -f \ - $(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \ - down -v + @docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v clear-standalone-data: @docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \ diff --git a/README.md b/README.md index 6b45706c44..d00adedcc8 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,12 @@ We support [OpenTelemetry](https://opentelemetry.io) as the library which you ca - Python - NodeJS - Go +- PHP +- .NET +- Ruby +- Elixir +- Rust + You can find the complete list of languages here - https://opentelemetry.io/docs/ diff --git a/README.zh-cn.md b/README.zh-cn.md index d584d10f4e..3658eeb520 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -13,14 +13,19 @@ ## -SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式跟踪来增加软件技术栈的可见性。 +SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式追踪来增加软件技术栈的可见性。 -👉 你能看到一些性能矩阵,服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。 +👉 你能看到一些性能指标,服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。 -👉 通过准确的跟踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。 +👉 通过准确的追踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。 +👉 聚合trace数据来获得业务相关指标。 -![SigNoz Feature](https://signoz-public.s3.us-east-2.amazonaws.com/signoz_hero_github.png) +![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)

@@ -36,12 +41,12 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo ## 功能: -- 应用总览矩阵(matrix),如RPS, 50/90/99百分比延迟率,错误率 +- 应用概览指标(metrics),如RPS, p50/p90/p99延迟率分位值,错误率等。 - 应用中最慢的终端(endpoint) -- 查看准确的网络请求跟踪来分析下游服务问题、慢数据库查询问题 及调用第三方服务如支付网关的问题 -- 通过服务名称、操作、延迟、错误、标签来过滤跟踪 -- 对过滤后的跟踪数据做矩阵聚合。比如,获得过滤条件`customer_type: gold` or `deployment_version: v2` or `external_call: paypal`的错误率和p99延迟 -- 整合的矩阵和跟踪用户界面。不需要像从Prometheus切换到Jaeger才能调试问题 +- 查看特定请求的trace数据来分析下游服务问题、慢数据库查询问题 及调用第三方服务如支付网关的问题 +- 通过服务名称、操作、延迟、错误、标签来过滤traces。 +- 聚合trace数据(events/spans)来得到业务相关指标。比如,你可以通过过滤条件`customer_type: gold` or `deployment_version: v2` or `external_call: paypal` 来获取指定业务的错误率和p99延迟 +- 为metrics和trace提供统一的UI。排查问题不需要在Prometheus和Jaeger之间切换。

@@ -53,7 +58,7 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo 我们想做一个自服务的开源版本的工具,类似于DataDog和NewRelic,用于那些对客户数据流入第三方有隐私和安全担忧的厂商。 -开源也让你对配置、采样和上线率有完整的控制,你可以在SigNoz基础上构建模块来满足特定的商业需求。 +开源也让你对配置、采样和正常运行时间有完整的控制,你可以在SigNoz基础上构建模块来满足特定的商业需求。 ### 语言支持 @@ -71,8 +76,8 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo ## 入门 - - + + ### 使用Docker部署 请按照[这里](https://signoz.io/docs/deployment/docker/)列出的步骤使用Docker来安装 @@ -80,35 +85,34 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo 如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/deployment/troubleshooting)会对你有帮助。

 

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

-## Comparisons to Familiar Tools +## 与其他方案的比较 ### SigNoz vs Prometheus -如果你只是需要矩阵,那Prometheus是不错的,但如果你要无缝的在矩阵和跟踪之间切换,那目前把Prometheus & Jaeger串起来的体验并不好。 +如果你只是需要监控指标(metrics),那Prometheus是不错的,但如果你要无缝的在metrics和traces之间切换,那目前把Prometheus & Jaeger串起来的体验并不好。 -我们的目标是在矩阵和跟踪之间提供整合的UI - 类似于Datadog这样的Saas厂提供的方案,能够对跟踪进行过滤和聚合,这是目前Jaeger缺失的功能。 +我们的目标是为metrics和traces提供统一的UI - 类似于Datadog这样的Saas厂提供的方案。并且能够对trace进行过滤和聚合,这是目前Jaeger缺失的功能。

 

### SigNoz vs Jaeger -Jaeger只做分布式跟踪,SigNoz则是做了矩阵和跟踪两块,我们在计划中也有日志管理功能。 +Jaeger只做分布式追踪(distributed tracing),SigNoz则支持metrics,traces,logs ,即可视化的三大支柱。 并且SigNoz有一些Jaeger没有的高级功能: -- Jaegar UI无法在跟踪或过滤的跟踪基础上展示矩阵。 -- Jaeger不能在过滤的跟踪上进行聚合操作。例如,拥有tag为customer_type='premium'的所有请求的p99延迟,在SigNoz里这很容易实现。 +- Jaegar UI无法在traces或过滤的traces上展示metrics。 +- Jaeger不能对过滤的traces做聚合操作。例如,拥有tag为customer_type='premium'的所有请求的p99延迟。而这个功能在SigNoz这儿是很容易实现。

@@ -121,6 +125,23 @@ Jaeger只做分布式跟踪,SigNoz则是做了矩阵和跟踪两块,我们 还不清楚怎么开始? 只需在[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) +

diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 5bc37de791..88380538d7 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -2,7 +2,7 @@ version: "3.9" services: clickhouse: - image: clickhouse/clickhouse-server:22.4.5-alpine + image: clickhouse/clickhouse-server:22.8.8-alpine # ports: # - "9000:9000" # - "8123:8123" @@ -40,7 +40,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.11.2 + image: signoz/query-service:0.11.3 command: ["-config=/root/config/prometheus.yml"] # ports: # - "6060:6060" # pprof port @@ -70,7 +70,7 @@ services: - clickhouse frontend: - image: signoz/frontend:0.11.2 + image: signoz/frontend:0.11.3 deploy: restart_policy: condition: on-failure @@ -83,7 +83,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.55.3 + image: signoz/signoz-otel-collector:0.63.0 command: ["--config=/etc/otel-collector-config.yaml"] user: root # required for reading docker container logs volumes: @@ -111,7 +111,7 @@ services: - clickhouse otel-collector-metrics: - image: signoz/signoz-otel-collector:0.55.3 + image: signoz/signoz-otel-collector:0.63.0 command: ["--config=/etc/otel-collector-metrics-config.yaml"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml index 0a54841d0c..1c3b59c1c5 100644 --- a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml @@ -47,7 +47,7 @@ receivers: # thrift_binary: # endpoint: 0.0.0.0:6832 hostmetrics: - collection_interval: 60s + collection_interval: 30s scrapers: cpu: {} load: {} @@ -55,6 +55,16 @@ receivers: disk: {} filesystem: {} network: {} + prometheus: + config: + global: + scrape_interval: 60s + scrape_configs: + # otel-collector internal metrics + - job_name: otel-collector + static_configs: + - targets: + - localhost:8888 processors: batch: @@ -65,7 +75,6 @@ processors: # Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels. detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure. timeout: 2s - override: false signozspanmetrics/prometheus: metrics_exporter: prometheus latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] @@ -134,8 +143,8 @@ service: receivers: [otlp] processors: [batch] exporters: [clickhousemetricswrite] - metrics/hostmetrics: - receivers: [hostmetrics] + metrics/generic: + receivers: [hostmetrics, prometheus] processors: [resourcedetection, batch] exporters: [clickhousemetricswrite] metrics/spanmetrics: diff --git a/deploy/docker-swarm/clickhouse-setup/otel-collector-metrics-config.yaml b/deploy/docker-swarm/clickhouse-setup/otel-collector-metrics-config.yaml index ecaee5977a..1786eb42e3 100644 --- a/deploy/docker-swarm/clickhouse-setup/otel-collector-metrics-config.yaml +++ b/deploy/docker-swarm/clickhouse-setup/otel-collector-metrics-config.yaml @@ -2,27 +2,19 @@ receivers: prometheus: config: scrape_configs: - # otel-collector internal metrics - - job_name: "otel-collector" - scrape_interval: 60s - dns_sd_configs: - - names: - - 'tasks.otel-collector' - type: 'A' - port: 8888 # otel-collector-metrics internal metrics - - job_name: "otel-collector-metrics" + - job_name: otel-collector-metrics scrape_interval: 60s static_configs: - targets: - localhost:8888 # SigNoz span metrics - - job_name: "signozspanmetrics-collector" + - job_name: signozspanmetrics-collector scrape_interval: 60s dns_sd_configs: - names: - - 'tasks.otel-collector' - type: 'A' + - tasks.otel-collector + type: A port: 8889 processors: diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index a7d265d3f5..45a7043a0a 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -2,7 +2,7 @@ version: "2.4" services: clickhouse: - image: clickhouse/clickhouse-server:22.4.5-alpine + image: clickhouse/clickhouse-server:22.8.8-alpine container_name: clickhouse # ports: # - "9000:9000" @@ -41,7 +41,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` otel-collector: container_name: otel-collector - image: signoz/signoz-otel-collector:0.55.3 + image: signoz/signoz-otel-collector:0.63.0 command: ["--config=/etc/otel-collector-config.yaml"] # user: root # required for reading docker container logs volumes: @@ -67,7 +67,7 @@ services: otel-collector-metrics: container_name: otel-collector-metrics - image: signoz/signoz-otel-collector:0.55.3 + image: signoz/signoz-otel-collector:0.63.0 command: ["--config=/etc/otel-collector-metrics-config.yaml"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/deploy/docker/clickhouse-setup/docker-compose-prod.yaml b/deploy/docker/clickhouse-setup/docker-compose-prod.yaml deleted file mode 100644 index 2aa522026e..0000000000 --- a/deploy/docker/clickhouse-setup/docker-compose-prod.yaml +++ /dev/null @@ -1,44 +0,0 @@ -version: "2.4" - -services: - query-service: - image: signoz/query-service:0.11.2 - container_name: query-service - command: ["-config=/root/config/prometheus.yml"] - # ports: - # - "6060:6060" # pprof port - # - "8080:8080" # query-service port - volumes: - - ./prometheus.yml:/root/config/prometheus.yml - - ../dashboards:/root/config/dashboards - - ./data/signoz/:/var/lib/signoz/ - environment: - - ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces - - ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/ - - SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db - - DASHBOARDS_PATH=/root/config/dashboards - - STORAGE=clickhouse - - GODEBUG=netdns=go - - TELEMETRY_ENABLED=true - - DEPLOYMENT_TYPE=docker-standalone-amd - restart: on-failure - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"] - interval: 30s - timeout: 5s - retries: 3 - depends_on: - clickhouse: - condition: service_healthy - - frontend: - image: signoz/frontend:0.11.2 - container_name: frontend - restart: on-failure - depends_on: - - alertmanager - - query-service - ports: - - "3301:3301" - volumes: - - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 964a835e26..1f877ffc78 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -2,7 +2,7 @@ version: "2.4" services: clickhouse: - image: clickhouse/clickhouse-server:22.4.5-alpine + image: clickhouse/clickhouse-server:22.8.8-alpine # ports: # - "9000:9000" # - "8123:8123" @@ -39,7 +39,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:0.11.2 + image: signoz/query-service:0.11.3 container_name: query-service command: ["-config=/root/config/prometheus.yml"] # ports: @@ -69,7 +69,7 @@ services: condition: service_healthy frontend: - image: signoz/frontend:0.11.2 + image: signoz/frontend:0.11.3 container_name: frontend restart: on-failure depends_on: @@ -81,7 +81,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.55.3 + image: signoz/signoz-otel-collector:0.63.0 command: ["--config=/etc/otel-collector-config.yaml"] user: root # required for reading docker container logs volumes: @@ -107,7 +107,7 @@ services: condition: service_healthy otel-collector-metrics: - image: signoz/signoz-otel-collector:0.55.3 + image: signoz/signoz-otel-collector:0.63.0 command: ["--config=/etc/otel-collector-metrics-config.yaml"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/deploy/docker/clickhouse-setup/otel-collector-config.yaml b/deploy/docker/clickhouse-setup/otel-collector-config.yaml index b05b957e30..46bd7a87d4 100644 --- a/deploy/docker/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker/clickhouse-setup/otel-collector-config.yaml @@ -47,7 +47,7 @@ receivers: # thrift_binary: # endpoint: 0.0.0.0:6832 hostmetrics: - collection_interval: 60s + collection_interval: 30s scrapers: cpu: {} load: {} @@ -55,6 +55,16 @@ receivers: disk: {} filesystem: {} network: {} + prometheus: + config: + global: + scrape_interval: 60s + scrape_configs: + # otel-collector internal metrics + - job_name: otel-collector + static_configs: + - targets: + - localhost:8888 processors: batch: @@ -89,7 +99,6 @@ processors: # Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels. detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure. timeout: 2s - override: false extensions: health_check: @@ -138,8 +147,8 @@ service: receivers: [otlp] processors: [batch] exporters: [clickhousemetricswrite] - metrics/hostmetrics: - receivers: [hostmetrics] + metrics/generic: + receivers: [hostmetrics, prometheus] processors: [resourcedetection, batch] exporters: [clickhousemetricswrite] metrics/spanmetrics: diff --git a/deploy/docker/clickhouse-setup/otel-collector-metrics-config.yaml b/deploy/docker/clickhouse-setup/otel-collector-metrics-config.yaml index fdc5830f57..aecad4eaaf 100644 --- a/deploy/docker/clickhouse-setup/otel-collector-metrics-config.yaml +++ b/deploy/docker/clickhouse-setup/otel-collector-metrics-config.yaml @@ -6,20 +6,14 @@ receivers: prometheus: config: scrape_configs: - # otel-collector internal metrics - - job_name: "otel-collector" - scrape_interval: 60s - static_configs: - - targets: - - otel-collector:8888 # otel-collector-metrics internal metrics - - job_name: "otel-collector-metrics" + - job_name: otel-collector-metrics scrape_interval: 60s static_configs: - targets: - localhost:8888 # SigNoz span metrics - - job_name: "signozspanmetrics-collector" + - job_name: signozspanmetrics-collector scrape_interval: 60s static_configs: - targets: diff --git a/ee/query-service/license/manager.go b/ee/query-service/license/manager.go index 306fa5a8d1..a3e9ba0771 100644 --- a/ee/query-service/license/manager.go +++ b/ee/query-service/license/manager.go @@ -10,6 +10,8 @@ import ( "sync" + baseconstants "go.signoz.io/signoz/pkg/query-service/constants" + validate "go.signoz.io/signoz/ee/query-service/integrations/signozio" "go.signoz.io/signoz/ee/query-service/model" basemodel "go.signoz.io/signoz/pkg/query-service/model" @@ -92,6 +94,8 @@ func (lm *Manager) SetActive(l *model.License) { lm.activeLicense = l lm.activeFeatures = l.FeatureSet + // set default features + setDefaultFeatures(lm) if !lm.validatorRunning { // we want to make sure only one validator runs, // we already have lock() so good to go @@ -101,7 +105,13 @@ func (lm *Manager) SetActive(l *model.License) { } -// LoadActiveLicense loads the most recent active licenseex +func setDefaultFeatures(lm *Manager) { + for k, v := range baseconstants.DEFAULT_FEATURE_SET { + lm.activeFeatures[k] = v + } +} + +// LoadActiveLicense loads the most recent active license func (lm *Manager) LoadActiveLicense() error { var err error active, err := lm.repo.GetActiveLicense(context.Background()) @@ -111,7 +121,10 @@ func (lm *Manager) LoadActiveLicense() error { if active != nil { lm.SetActive(active) } else { - zap.S().Info("No active license found.") + zap.S().Info("No active license found, defaulting to basic plan") + // if no active license is found, we default to basic(free) plan with all default features + lm.activeFeatures = basemodel.BasicPlan + setDefaultFeatures(lm) } return nil @@ -278,8 +291,11 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m // CheckFeature will be internally used by backend routines // for feature gating func (lm *Manager) CheckFeature(featureKey string) error { - if _, ok := lm.activeFeatures[featureKey]; ok { - return nil + if value, ok := lm.activeFeatures[featureKey]; ok { + if value { + return nil + } + return basemodel.ErrFeatureUnavailable{Key: featureKey} } return basemodel.ErrFeatureUnavailable{Key: featureKey} } diff --git a/frontend/public/locales/en-GB/dashboard.json b/frontend/public/locales/en-GB/dashboard.json index 7f21149511..b643f4727d 100644 --- a/frontend/public/locales/en-GB/dashboard.json +++ b/frontend/public/locales/en-GB/dashboard.json @@ -1,6 +1,7 @@ { "create_dashboard": "Create Dashboard", "import_json": "Import JSON", + "import_grafana_json": "Import Grafana JSON", "copy_to_clipboard": "Copy To ClipBoard", "download_json": "Download JSON", "view_json": "View JSON", diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index 7f21149511..b643f4727d 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -1,6 +1,7 @@ { "create_dashboard": "Create Dashboard", "import_json": "Import JSON", + "import_grafana_json": "Import Grafana JSON", "copy_to_clipboard": "Copy To ClipBoard", "download_json": "Download JSON", "view_json": "View JSON", diff --git a/frontend/src/api/dashboard/create.ts b/frontend/src/api/dashboard/create.ts index ab2ace2144..3796eb685e 100644 --- a/frontend/src/api/dashboard/create.ts +++ b/frontend/src/api/dashboard/create.ts @@ -7,8 +7,9 @@ import { PayloadProps, Props } from 'types/api/dashboard/create'; const create = async ( props: Props, ): Promise | ErrorResponse> => { + const url = props.uploadedGrafana ? '/dashboards/grafana' : '/dashboards'; try { - const response = await axios.post('/dashboards', { + const response = await axios.post(url, { ...props, }); diff --git a/frontend/src/api/dynamicConfigs/getDynamicConfigs.ts b/frontend/src/api/dynamicConfigs/getDynamicConfigs.ts new file mode 100644 index 0000000000..149c113119 --- /dev/null +++ b/frontend/src/api/dynamicConfigs/getDynamicConfigs.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 { PayloadProps } from 'types/api/dynamicConfigs/getDynamicConfigs'; + +const getDynamicConfigs = async (): Promise< + SuccessResponse | ErrorResponse +> => { + try { + const response = await axios.get(`/configs`); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getDynamicConfigs; diff --git a/frontend/src/constants/app.ts b/frontend/src/constants/app.ts index 68bfe983db..8529db4e4d 100644 --- a/frontend/src/constants/app.ts +++ b/frontend/src/constants/app.ts @@ -11,3 +11,5 @@ export const INVITE_MEMBERS_HASH = '#invite-team-members'; export const SIGNOZ_UPGRADE_PLAN_URL = 'https://upgrade.signoz.io/upgrade-from-app'; + +export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval'; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 9d3dbb88f7..3ff1cfe6c4 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -1,4 +1,5 @@ import { notification } from 'antd'; +import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs'; import getFeaturesFlags from 'api/features/getFeatureFlags'; import getUserLatestVersion from 'api/user/getLatestVersion'; import getUserVersion from 'api/user/getVersion'; @@ -14,6 +15,7 @@ import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { + UPDATE_CONFIGS, UPDATE_CURRENT_ERROR, UPDATE_CURRENT_VERSION, UPDATE_FEATURE_FLAGS, @@ -33,6 +35,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { getUserVersionResponse, getUserLatestVersionResponse, getFeaturesResponse, + getDynamicConfigsResponse, ] = useQueries([ { queryFn: getUserVersion, @@ -48,6 +51,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element { queryFn: getFeaturesFlags, queryKey: 'getFeatureFlags', }, + { + queryFn: getDynamicConfigs, + queryKey: 'getDynamicConfigs', + }, ]); useEffect(() => { @@ -65,11 +72,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element { if (getFeaturesResponse.status === 'idle') { getFeaturesResponse.refetch(); } + if (getDynamicConfigsResponse.status === 'idle') { + getDynamicConfigsResponse.refetch(); + } }, [ getFeaturesResponse, getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn, + getDynamicConfigsResponse, ]); const { children } = props; @@ -78,6 +89,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const latestCurrentCounter = useRef(0); const latestVersionCounter = useRef(0); + const latestConfigCounter = useRef(0); useEffect(() => { if ( @@ -170,6 +182,23 @@ function AppLayout(props: AppLayoutProps): JSX.Element { }, }); } + + if ( + getDynamicConfigsResponse.isFetched && + getDynamicConfigsResponse.isSuccess && + getDynamicConfigsResponse.data && + getDynamicConfigsResponse.data.payload && + latestConfigCounter.current === 0 + ) { + latestConfigCounter.current = 1; + + dispatch({ + type: UPDATE_CONFIGS, + payload: { + configs: getDynamicConfigsResponse.data.payload, + }, + }); + } }, [ dispatch, isLoggedIn, @@ -187,6 +216,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element { getFeaturesResponse.isFetched, getFeaturesResponse.isSuccess, getFeaturesResponse.data, + getDynamicConfigsResponse.data, + getDynamicConfigsResponse.isFetched, + getDynamicConfigsResponse.isSuccess, ]); const isToDisplayLayout = isLoggedIn; diff --git a/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx b/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx new file mode 100644 index 0000000000..84ac44e60e --- /dev/null +++ b/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx @@ -0,0 +1,33 @@ +import React, { PureComponent } from 'react'; + +interface State { + hasError: boolean; +} + +interface Props { + children: JSX.Element; +} + +class ErrorLink extends PureComponent { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(): State { + return { hasError: true }; + } + + render(): JSX.Element { + const { children } = this.props; + const { hasError } = this.state; + + if (hasError) { + return
; + } + + return children; + } +} + +export default ErrorLink; diff --git a/frontend/src/container/ConfigDropdown/Config/Link.tsx b/frontend/src/container/ConfigDropdown/Config/Link.tsx new file mode 100644 index 0000000000..2cc39b7779 --- /dev/null +++ b/frontend/src/container/ConfigDropdown/Config/Link.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +function LinkContainer({ children, href }: LinkContainerProps): JSX.Element { + const isInternalLink = href.startsWith('/'); + + if (isInternalLink) { + return {children}; + } + + return ( + + {children} + + ); +} + +interface LinkContainerProps { + children: React.ReactNode; + href: string; +} + +export default LinkContainer; diff --git a/frontend/src/container/ConfigDropdown/Config/index.tsx b/frontend/src/container/ConfigDropdown/Config/index.tsx new file mode 100644 index 0000000000..956ec5aa00 --- /dev/null +++ b/frontend/src/container/ConfigDropdown/Config/index.tsx @@ -0,0 +1,51 @@ +import { Menu, Space } from 'antd'; +import Spinner from 'components/Spinner'; +import React, { Suspense, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs'; +import AppReducer from 'types/reducer/app'; + +import ErrorLink from './ErrorLink'; +import LinkContainer from './Link'; + +function HelpToolTip({ config }: HelpToolTipProps): JSX.Element { + const sortedConfig = useMemo( + () => config.components.sort((a, b) => a.position - b.position), + [config.components], + ); + + const { isDarkMode } = useSelector((state) => state.app); + + return ( + + {sortedConfig.map((item) => { + const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`; + + const Component = React.lazy( + () => import(`@ant-design/icons/es/icons/${iconName}.js`), + ); + return ( + + }> + + + + + {item.text} + + + + + + ); + })} + + ); +} + +interface HelpToolTipProps { + config: ConfigProps; +} + +export default HelpToolTip; diff --git a/frontend/src/container/ConfigDropdown/index.tsx b/frontend/src/container/ConfigDropdown/index.tsx new file mode 100644 index 0000000000..12992bf1a6 --- /dev/null +++ b/frontend/src/container/ConfigDropdown/index.tsx @@ -0,0 +1,67 @@ +import { + CaretDownFilled, + CaretUpFilled, + QuestionCircleFilled, + QuestionCircleOutlined, +} from '@ant-design/icons'; +import { Dropdown, Menu, Space } from 'antd'; +import React, { useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import AppReducer from 'types/reducer/app'; + +import HelpToolTip from './Config'; + +function DynamicConfigDropdown({ + frontendId, +}: DynamicConfigDropdownProps): JSX.Element { + const { configs, isDarkMode } = useSelector( + (state) => state.app, + ); + const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState(false); + + const config = useMemo( + () => + Object.values(configs).find( + (config) => config.frontendPositionId === frontendId, + ), + [frontendId, configs], + ); + + const onToggleHandler = (): void => { + setIsHelpDropDownOpen(!isHelpDropDownOpen); + }; + + if (!config) { + return
; + } + + const Icon = isDarkMode ? QuestionCircleOutlined : QuestionCircleFilled; + const DropDownIcon = isHelpDropDownOpen ? CaretUpFilled : CaretDownFilled; + + return ( + + + + } + visible={isHelpDropDownOpen} + > + + + + + + ); +} + +interface DynamicConfigDropdownProps { + frontendId: string; +} + +export default DynamicConfigDropdown; diff --git a/frontend/src/container/Header/index.tsx b/frontend/src/container/Header/index.tsx index d6e4f79ae5..5deaa07bd2 100644 --- a/frontend/src/container/Header/index.tsx +++ b/frontend/src/container/Header/index.tsx @@ -14,8 +14,9 @@ import { } from 'antd'; import { Logout } from 'api/utils'; import ROUTES from 'constants/routes'; +import Config from 'container/ConfigDropdown'; import setTheme, { AppMode } from 'lib/theme/setTheme'; -import React, { useCallback, useState } from 'react'; +import React, { Dispatch, SetStateAction, useCallback, useState } from 'react'; import { connect, useSelector } from 'react-redux'; import { NavLink } from 'react-router-dom'; import { bindActionCreators } from 'redux'; @@ -28,13 +29,19 @@ import AppReducer from 'types/reducer/app'; import CurrentOrganization from './CurrentOrganization'; import ManageLicense from './ManageLicense'; import SignedInAS from './SignedInAs'; -import { Container, LogoutContainer, ToggleButton } from './styles'; +import { + Container, + IconContainer, + LogoutContainer, + ToggleButton, +} from './styles'; function HeaderContainer({ toggleDarkMode }: Props): JSX.Element { const { isDarkMode, user, currentVersion } = useSelector( (state) => state.app, ); - const [isUserDropDownOpen, setIsUserDropDownOpen] = useState(); + + const [isUserDropDownOpen, setIsUserDropDownOpen] = useState(false); const onToggleThemeHandler = useCallback(() => { const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode'; @@ -57,22 +64,21 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element { }; }, [toggleDarkMode, isDarkMode]); - const onArrowClickHandler: VoidFunction = () => { - setIsUserDropDownOpen((state) => !state); - }; - - const onClickLogoutHandler = (): void => { - Logout(); - }; + const onToggleHandler = useCallback( + (functionToExecute: Dispatch>) => (): void => { + functionToExecute((state) => !state); + }, + [], + ); const menu = ( - + - + @@ -80,11 +86,11 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element { tabIndex={0} onKeyDown={(e): void => { if (e.key === 'Enter' || e.key === 'Space') { - onClickLogoutHandler(); + Logout(); } }} role="button" - onClick={onClickLogoutHandler} + onClick={Logout} > Logout
@@ -94,23 +100,20 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element { ); return ( - + - - SigNoz - - SigNoz - + + + SigNoz + + SigNoz + + - + + + + {user?.name[0]} - {!isUserDropDownOpen ? ( - - ) : ( - - )} + + {!isUserDropDownOpen ? : } + diff --git a/frontend/src/container/Header/styles.ts b/frontend/src/container/Header/styles.ts index 602634f356..01b90559cc 100644 --- a/frontend/src/container/Header/styles.ts +++ b/frontend/src/container/Header/styles.ts @@ -4,6 +4,7 @@ import styled from 'styled-components'; export const Container = styled.div` display: flex; justify-content: space-between; + height: 100%; `; export const AvatarContainer = styled.div` @@ -61,3 +62,7 @@ export const ToggleButton = styled(Switch)` font-size: 1rem !important; } `; + +export const IconContainer = styled.div` + color: white; +`; diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index e1107fd9f2..60f429a744 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -26,6 +26,7 @@ import { EditorContainer, FooterContainer } from './styles'; function ImportJSON({ isImportJSONModalVisible, + uploadedGrafana, onModalHandler, }: ImportJSONProps): JSX.Element { const [jsonData, setJsonData] = useState>(); @@ -89,6 +90,7 @@ function ImportJSON({ const response = await createDashboard({ ...parsedWidgets, + uploadedGrafana, }); if (response.statusCode === 200) { @@ -186,6 +188,7 @@ function ImportJSON({ interface ImportJSONProps { isImportJSONModalVisible: boolean; onModalHandler: VoidFunction; + uploadedGrafana: boolean; } export default ImportJSON; diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index cb375b9742..a2d1b0c37a 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -57,6 +57,7 @@ function ListOfAllDashboard(): JSX.Element { isImportJSONModalVisible, setIsImportJSONModalVisible, ] = useState(false); + const [uploadedGrafana, setUploadedGrafana] = useState(false); const [filteredDashboards, setFilteredDashboards] = useState(); @@ -137,6 +138,7 @@ function ListOfAllDashboard(): JSX.Element { title: t('new_dashboard_title', { ns: 'dashboard', }), + uploadedGrafana: false, }); if (response.statusCode === 200) { @@ -182,8 +184,9 @@ function ListOfAllDashboard(): JSX.Element { newDashboardState.loading, ]); - const onModalHandler = (): void => { + const onModalHandler = (uploadedGrafana: boolean): void => { setIsImportJSONModalVisible((state) => !state); + setUploadedGrafana(uploadedGrafana); }; const menu = useMemo( @@ -198,9 +201,18 @@ function ListOfAllDashboard(): JSX.Element { {t('create_dashboard')} )} - + onModalHandler(false)} + key={t('import_json').toString()} + > {t('import_json')} + onModalHandler(true)} + key={t('import_grafana_json').toString()} + > + {t('import_grafana_json')} + ), [createNewDashboard, loading, onNewDashboardHandler, t], @@ -256,7 +268,8 @@ function ListOfAllDashboard(): JSX.Element { onModalHandler(false)} /> { - return new URLSearchParams(search); - }, [search]); + const urlQuery = useUrlQuery(); const dispatch = useDispatch(); @@ -58,6 +51,8 @@ function Logs({ getLogsFields }: LogsProps): JSX.Element { ); } +type LogsProps = DispatchProps; + interface DispatchProps { getLogsFields: () => (dispatch: Dispatch) => void; } diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx index 1014fa7945..0723d2378e 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx @@ -22,10 +22,11 @@ import { v4 } from 'uuid'; import FieldKey from '../FieldKey'; import { QueryConditionContainer, QueryFieldContainer } from '../styles'; import { createParsedQueryStructure } from '../utils'; +import { hashCode, parseQuery } from './utils'; const { Option } = Select; interface QueryFieldProps { - query: { value: string | string[]; type: string }[]; + query: Query; queryIndex: number; onUpdate: (query: unknown, queryIndex: number) => void; onDelete: (queryIndex: number) => void; @@ -49,12 +50,12 @@ function QueryField({ } return ''; }; + const fieldType = useMemo(() => getFieldType(query[0].value as string), [ query, ]); const handleChange = (qIdx: number, value: string): void => { query[qIdx].value = value || ''; - if (qIdx === 1) { if (Object.values(QueryOperatorsMultiVal).includes(value)) { if (!Array.isArray(query[2].value)) { @@ -166,17 +167,8 @@ function QueryConditionField({ ); } -const hashCode = (s: string): string => { - if (!s) { - return '0'; - } - return `${Math.abs( - s.split('').reduce((a, b) => { - a = (a << 5) - a + b.charCodeAt(0); - return a & a; - }, 0), - )}`; -}; + +export type Query = { value: string | string[]; type: string }[]; function QueryBuilder({ updateParsedQuery, @@ -201,12 +193,9 @@ function QueryBuilder({ } }, [parsedQuery]); - const handleUpdate = ( - query: { value: string | string[]; type: string }[], - queryIndex: number, - ): void => { + const handleUpdate = (query: Query, queryIndex: number): void => { const updatedParsedQuery = generatedQueryStructure; - updatedParsedQuery[queryIndex] = query as never; + updatedParsedQuery[queryIndex] = parseQuery(query) as never; const flatParsedQuery = flatten(updatedParsedQuery).filter((q) => q.value); keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery)); diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/utils.ts b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/utils.ts new file mode 100644 index 0000000000..2641d8af35 --- /dev/null +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/utils.ts @@ -0,0 +1,37 @@ +import _set from 'lodash-es/set'; + +import { Query } from './QueryBuilder'; + +export const parseQuery = (queries: Query): Query => { + if (Array.isArray(queries)) { + const isContainsPresent = queries.find((e) => e.value === 'CONTAINS'); + if (isContainsPresent) { + // find the index of VALUE to update + const valueIndex = queries.findIndex((e) => e.type === 'QUERY_VALUE'); + if (valueIndex > -1) { + // update the value to wrap with "" + _set( + queries, + [valueIndex, 'value'], + `'${queries[valueIndex].value || ''}'`, + ); + } + return queries; + } + } + return queries; +}; + +export const hashCode = (s: string): string => { + if (!s) { + return '0'; + } + return `${Math.abs( + s.split('').reduce((a, b) => { + // eslint-disable-next-line no-bitwise, no-param-reassign + a = (a << 5) - a + b.charCodeAt(0); + // eslint-disable-next-line no-bitwise + return a & a; + }, 0), + )}`; +}; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/utils.ts b/frontend/src/container/LogsSearchFilter/SearchFields/utils.ts index 059392f36d..ae091dc061 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/utils.ts +++ b/frontend/src/container/LogsSearchFilter/SearchFields/utils.ts @@ -2,9 +2,14 @@ // @ts-ignore // @ts-nocheck -import { QueryTypes } from 'lib/logql/tokens'; +import { QueryTypes, QueryOperatorsSingleVal } from 'lib/logql/tokens'; -export const queryKOVPair = () => [ +export interface QueryFields { + type: keyof typeof QueryTypes; + value: string; +} + +export const queryKOVPair = (): QueryFields[] => [ { type: QueryTypes.QUERY_KEY, value: null, diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 974e77f7fe..7997fac91f 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -15,9 +15,6 @@ import { ILogsReducer } from 'types/reducer/logs'; import { Container, Heading } from './styles'; -interface LogsTableProps { - getLogs: (props: Parameters[0]) => ReturnType; -} function LogsTable({ getLogs }: LogsTableProps): JSX.Element { const { searchFilter: { queryString }, @@ -51,6 +48,7 @@ function LogsTable({ getLogs }: LogsTableProps): JSX.Element { if (isLoading) { return ; } + return ( @@ -86,4 +84,8 @@ const mapDispatchToProps = ( getLogs: bindActionCreators(getLogs, dispatch), }); +interface LogsTableProps { + getLogs: (props: Parameters[0]) => ReturnType; +} + export default connect(null, mapDispatchToProps)(memo(LogsTable)); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 7197887be1..082e6514e6 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -51,7 +51,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element { history.replace( `${ ROUTES.TRACE - }?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1&spanAggregateOrder=ascend`, + }?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1`, ); }; @@ -103,7 +103,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element { history.replace( `${ ROUTES.TRACE - }?${urlParams.toString()}?selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1&spanAggregateOrder=ascend`, + }?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1`, ); }; diff --git a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx index 4f91a97781..0048482837 100644 --- a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx +++ b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx @@ -42,7 +42,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { history.push( `${ ROUTES.TRACE - }?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false,"operation":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&spanAggregateCurrentPage=1&spanAggregateOrder=ascend`, + }?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false,"operation":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&spanAggregateCurrentPage=1`, ); }; diff --git a/frontend/src/container/TopNav/AutoRefresh/config.ts b/frontend/src/container/TopNav/AutoRefresh/config.ts new file mode 100644 index 0000000000..cefd0c8bf1 --- /dev/null +++ b/frontend/src/container/TopNav/AutoRefresh/config.ts @@ -0,0 +1,63 @@ +export const options: IOptions[] = [ + { + label: 'off', + key: 'off', + value: 0, + }, + { + label: '5s', + key: '5s', + value: 5000, + }, + { + label: '10s', + key: '10s', + value: 10000, + }, + { + label: '30s', + key: '30s', + value: 30000, + }, + { + label: '1m', + key: '1m', + value: 60000, + }, + { + label: '5m', + key: '5m', + value: 300000, + }, + { + label: '10m', + key: '10m', + value: 600000, + }, + { + label: '30m', + key: '30m', + value: 1800000, + }, + { + label: '1h', + key: '1h', + value: 3600000, + }, + { + label: '2h', + key: '2h', + value: 7200000, + }, + { + label: '1d', + key: '1d', + value: 86400000, + }, +]; + +export interface IOptions { + label: string; + key: string; + value: number; +} diff --git a/frontend/src/container/TopNav/AutoRefresh/index.tsx b/frontend/src/container/TopNav/AutoRefresh/index.tsx new file mode 100644 index 0000000000..bc52e2cf86 --- /dev/null +++ b/frontend/src/container/TopNav/AutoRefresh/index.tsx @@ -0,0 +1,166 @@ +import { CaretDownFilled } from '@ant-design/icons'; +import { + Checkbox, + Divider, + Popover, + Radio, + RadioChangeEvent, + Space, + Typography, +} from 'antd'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; +import get from 'api/browser/localstorage/get'; +import set from 'api/browser/localstorage/set'; +import { DASHBOARD_TIME_IN_DURATION } from 'constants/app'; +import dayjs from 'dayjs'; +import useUrlQuery from 'hooks/useUrlQuery'; +import _omit from 'lodash-es/omit'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { useInterval } from 'react-use'; +import { Dispatch } from 'redux'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { options } from './config'; +import { ButtonContainer, Container } from './styles'; + +function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { + const { minTime: initialMinTime, selectedTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + const { pathname } = useLocation(); + + const localStorageData = JSON.parse(get(DASHBOARD_TIME_IN_DURATION) || '{}'); + + const localStorageValue = useMemo(() => localStorageData[pathname], [ + pathname, + localStorageData, + ]); + + const [isAutoRefreshEnabled, setIsAutoRefreshfreshEnabled] = useState( + Boolean(localStorageValue), + ); + + useEffect(() => { + setIsAutoRefreshfreshEnabled(Boolean(localStorageValue)); + }, [localStorageValue]); + + const params = useUrlQuery(); + + const dispatch = useDispatch>(); + const [selectedOption, setSelectedOption] = useState( + localStorageValue || options[0].key, + ); + + useEffect(() => { + setSelectedOption(localStorageValue || options[0].key); + }, [localStorageValue, params]); + + const getOption = useMemo( + () => options.find((option) => option.key === selectedOption), + [selectedOption], + ); + + useInterval(() => { + const selectedValue = getOption?.value; + + if (disabled || !isAutoRefreshEnabled) { + return; + } + + if (selectedOption !== 'off' && selectedValue) { + const min = initialMinTime / 1000000; + + dispatch({ + type: UPDATE_TIME_INTERVAL, + payload: { + maxTime: dayjs().valueOf() * 1000000, + minTime: dayjs(min).subtract(selectedValue, 'second').valueOf() * 1000000, + selectedTime, + }, + }); + } + }, getOption?.value || 0); + + const onChangeHandler = useCallback( + (event: RadioChangeEvent) => { + const selectedValue = event.target.value; + setSelectedOption(selectedValue); + params.set(DASHBOARD_TIME_IN_DURATION, selectedValue); + set( + DASHBOARD_TIME_IN_DURATION, + JSON.stringify({ ...localStorageData, [pathname]: selectedValue }), + ); + setIsAutoRefreshfreshEnabled(true); + }, + [params, pathname, localStorageData], + ); + + const onChangeAutoRefreshHandler = useCallback( + (event: CheckboxChangeEvent) => { + const { checked } = event.target; + if (!checked) { + // remove the path from localstorage + set( + DASHBOARD_TIME_IN_DURATION, + JSON.stringify(_omit(localStorageData, pathname)), + ); + } + setIsAutoRefreshfreshEnabled(checked); + }, + [localStorageData, pathname], + ); + + return ( + + + Auto Refresh + + + + + Refresh Interval + + + + {options + .filter((e) => e.label !== 'off') + .map((option) => ( + + {option.label} + + ))} + + + + } + > + + + + + ); +} + +interface AutoRefreshProps { + disabled?: boolean; +} + +AutoRefresh.defaultProps = { + disabled: false, +}; + +export default AutoRefresh; diff --git a/frontend/src/container/TopNav/AutoRefresh/styles.ts b/frontend/src/container/TopNav/AutoRefresh/styles.ts new file mode 100644 index 0000000000..9672a346e7 --- /dev/null +++ b/frontend/src/container/TopNav/AutoRefresh/styles.ts @@ -0,0 +1,13 @@ +import { Button } from 'antd'; +import styled from 'styled-components'; + +export const Container = styled.div` + min-width: 8rem; +`; + +export const ButtonContainer = styled(Button)` + &&& { + padding-left: 0.5rem; + padding-right: 0.5rem; + } +`; diff --git a/frontend/src/container/TopNav/DateTimeSelection/Refresh.tsx b/frontend/src/container/TopNav/DateTimeSelection/Refresh.tsx index 36b1163dda..f4597e5727 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/Refresh.tsx +++ b/frontend/src/container/TopNav/DateTimeSelection/Refresh.tsx @@ -2,7 +2,10 @@ import React, { useEffect, useState } from 'react'; import { RefreshTextContainer, Typography } from './styles'; -function RefreshText({ onLastRefreshHandler }: RefreshTextProps): JSX.Element { +function RefreshText({ + onLastRefreshHandler, + refreshButtonHidden, +}: RefreshTextProps): JSX.Element { const [refreshText, setRefreshText] = useState(''); // this is to update the refresh text @@ -19,7 +22,7 @@ function RefreshText({ onLastRefreshHandler }: RefreshTextProps): JSX.Element { }, [onLastRefreshHandler, refreshText]); return ( - + {refreshText} ); @@ -27,6 +30,7 @@ function RefreshText({ onLastRefreshHandler }: RefreshTextProps): JSX.Element { interface RefreshTextProps { onLastRefreshHandler: () => string; + refreshButtonHidden: boolean; } export default RefreshText; diff --git a/frontend/src/container/TopNav/DateTimeSelection/config.ts b/frontend/src/container/TopNav/DateTimeSelection/config.ts index 1654def09f..d327476e08 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelection/config.ts @@ -67,3 +67,19 @@ export const getOptions = (routes: string): Option[] => { } return Options; }; + +export const routesToSkip = [ + ROUTES.SETTINGS, + ROUTES.LIST_ALL_ALERT, + ROUTES.TRACE_DETAIL, + ROUTES.ALL_CHANNELS, + ROUTES.USAGE_EXPLORER, + ROUTES.INSTRUMENTATION, + ROUTES.VERSION, + ROUTES.ALL_DASHBOARD, + ROUTES.ORG_SETTINGS, + ROUTES.ERROR_DETAIL, + ROUTES.ALERTS_NEW, + ROUTES.EDIT_ALERTS, + ROUTES.LIST_ALL_ALERT, +]; diff --git a/frontend/src/container/TopNav/DateTimeSelection/index.tsx b/frontend/src/container/TopNav/DateTimeSelection/index.tsx index c170a7cef3..18e8dce4c8 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelection/index.tsx @@ -1,3 +1,4 @@ +import { SyncOutlined } from '@ant-design/icons'; import { Button, Select as DefaultSelect } from 'antd'; import getLocalStorageKey from 'api/browser/localstorage/get'; import setLocalStorageKey from 'api/browser/localstorage/set'; @@ -14,10 +15,11 @@ import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { GlobalReducer } from 'types/reducer/globalTime'; +import AutoRefresh from '../AutoRefresh'; import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal'; import { getDefaultOption, getOptions, Time } from './config'; import RefreshText from './Refresh'; -import { Container, Form, FormItem } from './styles'; +import { Form, FormContainer, FormItem } from './styles'; const { Option } = DefaultSelect; @@ -240,6 +242,8 @@ function DateTimeSelection({ setStartTime(dayjs(preStartTime)); setEndTime(dayjs(preEndTime)); + setRefreshButtonHidden(updatedTime === 'custom'); + updateTimeInterval(updatedTime, [preStartTime, preEndTime]); }, [ location.pathname, @@ -253,35 +257,44 @@ function DateTimeSelection({ ]); return ( - + <>
- onSelectHandler(value as Time)} - value={getInputLabel(startTime, endTime, selectedTime)} - data-testid="dropDown" - > - {options.map(({ value, label }) => ( - - ))} - + + onSelectHandler(value as Time)} + value={getInputLabel(startTime, endTime, selectedTime)} + data-testid="dropDown" + > + {options.map(({ value, label }) => ( + + ))} + - +
- {!checkRouteExists(history.location.pathname) && ( + {!isRouteToSkip && ( diff --git a/frontend/src/container/Trace/TraceTable/index.tsx b/frontend/src/container/Trace/TraceTable/index.tsx index 96fece7ba4..c532ff2683 100644 --- a/frontend/src/container/Trace/TraceTable/index.tsx +++ b/frontend/src/container/Trace/TraceTable/index.tsx @@ -3,7 +3,6 @@ import Table, { ColumnsType } from 'antd/lib/table'; import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; -import history from 'lib/history'; import omit from 'lodash-es/omit'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -195,7 +194,7 @@ function TraceTable(): JSX.Element { onClick: (event): void => { event.preventDefault(); event.stopPropagation(); - history.push(getLink(record)); + window.open(getLink(record)); }, })} pagination={{ diff --git a/frontend/src/store/actions/global.ts b/frontend/src/store/actions/global.ts index c2b88919da..0e7b2e172f 100644 --- a/frontend/src/store/actions/global.ts +++ b/frontend/src/store/actions/global.ts @@ -2,6 +2,7 @@ import { Time } from 'container/TopNav/DateTimeSelection/config'; import GetMinMax from 'lib/getMinMax'; import { Dispatch } from 'redux'; import AppActions from 'types/actions'; +import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; export const UpdateTimeInterval = ( interval: Time, @@ -11,7 +12,7 @@ export const UpdateTimeInterval = ( const { maxTime, minTime } = GetMinMax(interval, dateTimeRange); dispatch({ - type: 'UPDATE_TIME_INTERVAL', + type: UPDATE_TIME_INTERVAL, payload: { maxTime, minTime, diff --git a/frontend/src/store/reducers/app.ts b/frontend/src/store/reducers/app.ts index 3e18a4c957..6fbc48049d 100644 --- a/frontend/src/store/reducers/app.ts +++ b/frontend/src/store/reducers/app.ts @@ -8,6 +8,7 @@ import { LOGGED_IN, SIDEBAR_COLLAPSE, SWITCH_DARK_MODE, + UPDATE_CONFIGS, UPDATE_CURRENT_ERROR, UPDATE_CURRENT_VERSION, UPDATE_FEATURE_FLAGS, @@ -56,6 +57,7 @@ const InitialValue: InitialValueTypes = { isUserFetchingError: false, org: null, role: null, + configs: {}, }; const appReducer = ( @@ -210,6 +212,13 @@ const appReducer = ( }; } + case UPDATE_CONFIGS: { + return { + ...state, + configs: action.payload.configs, + }; + } + default: return state; } diff --git a/frontend/src/types/actions/app.ts b/frontend/src/types/actions/app.ts index 65264f5ca3..a2a4b90f39 100644 --- a/frontend/src/types/actions/app.ts +++ b/frontend/src/types/actions/app.ts @@ -23,6 +23,7 @@ export const UPDATE_USER = 'UPDATE_USER'; export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME'; export const UPDATE_ORG = 'UPDATE_ORG'; export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS'; +export const UPDATE_CONFIGS = 'UPDATE_CONFIGS'; export interface SwitchDarkMode { type: typeof SWITCH_DARK_MODE; @@ -115,6 +116,12 @@ export interface UpdateOrg { org: AppReducer['org']; }; } +export interface UpdateConfigs { + type: typeof UPDATE_CONFIGS; + payload: { + configs: AppReducer['configs']; + }; +} export type AppAction = | SwitchDarkMode @@ -129,4 +136,5 @@ export type AppAction = | UpdateUser | UpdateOrgName | UpdateOrg - | UpdateFeatureFlags; + | UpdateFeatureFlags + | UpdateConfigs; diff --git a/frontend/src/types/api/dashboard/create.ts b/frontend/src/types/api/dashboard/create.ts index 3c859edc6c..9b0e26457d 100644 --- a/frontend/src/types/api/dashboard/create.ts +++ b/frontend/src/types/api/dashboard/create.ts @@ -3,7 +3,8 @@ import { Dashboard, DashboardData } from './getAll'; export type Props = | { title: Dashboard['data']['title']; + uploadedGrafana: boolean; } - | DashboardData; + | { DashboardData: DashboardData; uploadedGrafana: boolean }; export type PayloadProps = Dashboard; diff --git a/frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts b/frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts new file mode 100644 index 0000000000..69e55e008d --- /dev/null +++ b/frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts @@ -0,0 +1,14 @@ +export interface ConfigProps { + enabled: boolean; + frontendPositionId: string; + components: Array<{ + href: string; + darkIcon: string; + lightIcon: string; + position: 1; + text: string; + }>; +} +export interface PayloadProps { + [key: string]: ConfigProps; +} diff --git a/frontend/src/types/reducer/app.ts b/frontend/src/types/reducer/app.ts index 5c10f31a83..d95fd3a77b 100644 --- a/frontend/src/types/reducer/app.ts +++ b/frontend/src/types/reducer/app.ts @@ -1,3 +1,4 @@ +import { PayloadProps as ConfigPayload } from 'types/api/dynamicConfigs/getDynamicConfigs'; import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags'; import { PayloadProps as OrgPayload } from 'types/api/user/getOrganization'; import { PayloadProps as UserPayload } from 'types/api/user/getUser'; @@ -26,4 +27,5 @@ export default interface AppReducer { role: ROLES | null; org: OrgPayload | null; featureFlags: null | FeatureFlagPayload; + configs: ConfigPayload; } diff --git a/go.mod b/go.mod index 74fca9afe3..8666887ad6 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/mattn/go-sqlite3 v1.14.8 github.com/minio/minio-go/v6 v6.0.57 + github.com/mitchellh/mapstructure v1.5.0 github.com/oklog/oklog v0.3.2 github.com/pkg/errors v0.9.1 github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f @@ -145,4 +146,4 @@ require ( k8s.io/client-go v8.0.0+incompatible // indirect ) -replace github.com/prometheus/prometheus => github.com/SigNoz/prometheus v1.9.74 +replace github.com/prometheus/prometheus => github.com/SigNoz/prometheus v1.9.76 diff --git a/go.sum b/go.sum index 6babcdcb67..ba38dc1069 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,8 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8 github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb h1:bneLSKPf9YUSFmafKx32bynV6QrzViL/s+ZDvQxH1E4= github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb/go.mod h1:JznGDNg9x1cujDKa22RaQOimOvvEfy3nxzDGd8XDgmA= -github.com/SigNoz/prometheus v1.9.74 h1:/AcKVZ80Cg4FQ/quMDLO4Ejyeb3KDjgUKveiN+OXVL8= -github.com/SigNoz/prometheus v1.9.74/go.mod h1:Y4J9tGDmacMC+EcOTp+EIAn2C1sN+9kE+idyVKadiVM= +github.com/SigNoz/prometheus v1.9.76 h1:YQOHezj4Yyu6PHV7/bVR297FQgUMQAAJtCVZ+NslwYk= +github.com/SigNoz/prometheus v1.9.76/go.mod h1:Y4J9tGDmacMC+EcOTp+EIAn2C1sN+9kE+idyVKadiVM= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -345,6 +345,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mkevac/debugcharts v0.0.0-20191222103121-ae1c48aa8615/go.mod h1:Ad7oeElCZqA1Ufj0U9/liOF4BtVepxRcTvr2ey7zTvM= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 009a372add..0aa037490e 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -2729,7 +2729,7 @@ func (r *ClickHouseReader) GetMetricAutocompleteTagKey(ctx context.Context, para tagsWhereClause := "" for key, val := range params.MetricTags { - tagsWhereClause += fmt.Sprintf(" AND labels_object.%s = '%s' ", key, val) + tagsWhereClause += fmt.Sprintf(" AND JSONExtractString(labels, '%s') = '%s' ", key, val) } // "select distinctTagKeys from (SELECT DISTINCT arrayJoin(tagKeys) distinctTagKeys from (SELECT DISTINCT(JSONExtractKeys(labels)) tagKeys from signoz_metrics.time_series WHERE JSONExtractString(labels,'__name__')='node_udp_queues')) WHERE distinctTagKeys ILIKE '%host%';" if len(params.Match) != 0 { @@ -2768,16 +2768,16 @@ func (r *ClickHouseReader) GetMetricAutocompleteTagValue(ctx context.Context, pa tagsWhereClause := "" for key, val := range params.MetricTags { - tagsWhereClause += fmt.Sprintf(" AND labels_object.%s = '%s' ", key, val) + tagsWhereClause += fmt.Sprintf(" AND JSONExtractString(labels, '%s') = '%s' ", key, val) } if len(params.Match) != 0 { - query = fmt.Sprintf("SELECT DISTINCT(labels_object.%s) from %s.%s WHERE metric_name=$1 %s AND labels_object.%s ILIKE $2;", params.TagKey, signozMetricDBName, signozTSTableName, tagsWhereClause, params.TagKey) + query = fmt.Sprintf("SELECT DISTINCT(JSONExtractString(labels, '%s')) from %s.%s WHERE metric_name=$1 %s AND JSONExtractString(labels, '%s') ILIKE $2;", params.TagKey, signozMetricDBName, signozTSTableName, tagsWhereClause, params.TagKey) rows, err = r.db.Query(ctx, query, params.TagKey, params.MetricName, fmt.Sprintf("%%%s%%", params.Match)) } else { - query = fmt.Sprintf("SELECT DISTINCT(labels_object.%s) FROM %s.%s WHERE metric_name=$2 %s;", params.TagKey, signozMetricDBName, signozTSTableName, tagsWhereClause) + query = fmt.Sprintf("SELECT DISTINCT(JSONExtractString(labels, '%s')) FROM %s.%s WHERE metric_name=$2 %s;", params.TagKey, signozMetricDBName, signozTSTableName, tagsWhereClause) rows, err = r.db.Query(ctx, query, params.TagKey, params.MetricName) } diff --git a/pkg/query-service/app/dashboards/model.go b/pkg/query-service/app/dashboards/model.go index 4969a18728..56adf9aae7 100644 --- a/pkg/query-service/app/dashboards/model.go +++ b/pkg/query-service/app/dashboards/model.go @@ -4,12 +4,16 @@ import ( "encoding/base64" "encoding/json" "fmt" + "reflect" + "regexp" + "strconv" "strings" "time" "github.com/google/uuid" "github.com/gosimple/slug" "github.com/jmoiron/sqlx" + "github.com/mitchellh/mapstructure" "go.signoz.io/signoz/pkg/query-service/model" "go.uber.org/zap" ) @@ -17,6 +21,14 @@ import ( // This time the global variable is unexported. var db *sqlx.DB +// User for mapping job,instance from grafana +var instanceEQRE = regexp.MustCompile("instance(?s)=(?s)\\\"{{.instance}}\\\"") +var nodeEQRE = regexp.MustCompile("instance(?s)=(?s)\\\"{{.node}}\\\"") +var jobEQRE = regexp.MustCompile("job(?s)=(?s)\\\"{{.job}}\\\"") +var instanceRERE = regexp.MustCompile("instance(?s)=~(?s)\\\"{{.instance}}\\\"") +var nodeRERE = regexp.MustCompile("instance(?s)=~(?s)\\\"{{.node}}\\\"") +var jobRERE = regexp.MustCompile("job(?s)=~(?s)\\\"{{.job}}\\\"") + // InitDB sets up setting up the connection pool global variable. func InitDB(dataSourceName string) (*sqlx.DB, error) { var err error @@ -260,3 +272,236 @@ func SlugifyTitle(title string) string { } return s } + +func widgetFromPanel(panel model.Panels, idx int, variables map[string]model.Variable) *model.Widget { + widget := model.Widget{ + Description: panel.Description, + ID: strconv.Itoa(idx), + IsStacked: false, + NullZeroValues: "zero", + Opacity: "1", + PanelTypes: "TIME_SERIES", // TODO: Need to figure out how to get this + Query: model.Query{ + ClickHouse: []model.ClickHouseQueryDashboard{ + { + Disabled: false, + Legend: "", + Name: "A", + Query: "", + }, + }, + MetricsBuilder: model.MetricsBuilder{ + Formulas: []string{}, + QueryBuilder: []model.QueryBuilder{ + { + AggregateOperator: 1, + Disabled: false, + GroupBy: []string{}, + Legend: "", + MetricName: "", + Name: "A", + ReduceTo: 1, + }, + }, + }, + PromQL: []model.PromQueryDashboard{}, + QueryType: int(model.PROM), + }, + QueryData: model.QueryDataDashboard{ + Data: model.Data{ + QueryData: []interface{}{}, + }, + }, + Title: panel.Title, + YAxisUnit: panel.FieldConfig.Defaults.Unit, + QueryType: int(model.PROM), // TODO: Supprot for multiple query types + } + for _, target := range panel.Targets { + if target.Expr != "" { + for name := range variables { + target.Expr = strings.ReplaceAll(target.Expr, "$"+name, "{{"+"."+name+"}}") + target.Expr = strings.ReplaceAll(target.Expr, "$"+"__rate_interval", "5m") + } + + // prometheus receiver in collector maps job,instance as service_name,service_instance_id + target.Expr = instanceEQRE.ReplaceAllString(target.Expr, "service_instance_id=\"{{.instance}}\"") + target.Expr = nodeEQRE.ReplaceAllString(target.Expr, "service_instance_id=\"{{.node}}\"") + target.Expr = jobEQRE.ReplaceAllString(target.Expr, "service_name=\"{{.job}}\"") + target.Expr = instanceRERE.ReplaceAllString(target.Expr, "service_instance_id=~\"{{.instance}}\"") + target.Expr = nodeRERE.ReplaceAllString(target.Expr, "service_instance_id=~\"{{.node}}\"") + target.Expr = jobRERE.ReplaceAllString(target.Expr, "service_name=~\"{{.job}}\"") + + widget.Query.PromQL = append( + widget.Query.PromQL, + model.PromQueryDashboard{ + Disabled: false, + Legend: target.LegendFormat, + Name: target.RefID, + Query: target.Expr, + }, + ) + } + } + return &widget +} + +func TransformGrafanaJSONToSignoz(grafanaJSON model.GrafanaJSON) model.DashboardData { + var toReturn model.DashboardData + toReturn.Title = grafanaJSON.Title + toReturn.Tags = grafanaJSON.Tags + toReturn.Variables = make(map[string]model.Variable) + + for templateIdx, template := range grafanaJSON.Templating.List { + var sort, typ, textboxValue, customValue, queryValue string + if template.Sort == 1 { + sort = "ASC" + } else if template.Sort == 2 { + sort = "DESC" + } else { + sort = "DISABLED" + } + + if template.Type == "query" { + if template.Datasource == nil { + zap.S().Warnf("Skipping panel %d as it has no datasource", templateIdx) + continue + } + // Skip if the source is not prometheus + source, stringOk := template.Datasource.(string) + if stringOk && !strings.Contains(strings.ToLower(source), "prometheus") { + zap.S().Warnf("Skipping template %d as it is not prometheus", templateIdx) + continue + } + var result model.Datasource + var structOk bool + if reflect.TypeOf(template.Datasource).Kind() == reflect.Map { + err := mapstructure.Decode(template.Datasource, &result) + if err == nil { + structOk = true + } + } + if result.Type != "prometheus" && result.Type != "" { + zap.S().Warnf("Skipping template %d as it is not prometheus", templateIdx) + continue + } + + if !stringOk && !structOk { + zap.S().Warnf("Didn't recognize source, skipping") + continue + } + typ = "QUERY" + } else if template.Type == "custom" { + typ = "CUSTOM" + } else if template.Type == "textbox" { + typ = "TEXTBOX" + text, ok := template.Current.Text.(string) + if ok { + textboxValue = text + } + array, ok := template.Current.Text.([]string) + if ok { + textboxValue = strings.Join(array, ",") + } + } else { + continue + } + + var selectedValue string + text, ok := template.Current.Value.(string) + if ok { + selectedValue = text + } + array, ok := template.Current.Value.([]string) + if ok { + selectedValue = strings.Join(array, ",") + } + + toReturn.Variables[template.Name] = model.Variable{ + AllSelected: false, + CustomValue: customValue, + Description: template.Label, + MultiSelect: template.Multi, + QueryValue: queryValue, + SelectedValue: selectedValue, + ShowALLOption: template.IncludeAll, + Sort: sort, + TextboxValue: textboxValue, + Type: typ, + } + } + + row := 0 + idx := 0 + for _, panel := range grafanaJSON.Panels { + if panel.Type == "row" { + if panel.Panels != nil && len(panel.Panels) > 0 { + for _, innerPanel := range panel.Panels { + if idx%3 == 0 { + row++ + } + toReturn.Layout = append( + toReturn.Layout, + model.Layout{ + X: idx % 3 * 4, + Y: row * 3, + W: 4, + H: 3, + I: strconv.Itoa(idx), + }, + ) + + toReturn.Widgets = append(toReturn.Widgets, *widgetFromPanel(innerPanel, idx, toReturn.Variables)) + idx++ + } + } + continue + } + if panel.Datasource == nil { + zap.S().Warnf("Skipping panel %d as it has no datasource", idx) + continue + } + // Skip if the datasource is not prometheus + source, stringOk := panel.Datasource.(string) + if stringOk && !strings.Contains(strings.ToLower(source), "prometheus") { + zap.S().Warnf("Skipping panel %d as it is not prometheus", idx) + continue + } + var result model.Datasource + var structOk bool + if reflect.TypeOf(panel.Datasource).Kind() == reflect.Map { + err := mapstructure.Decode(panel.Datasource, &result) + if err == nil { + structOk = true + } + } + if result.Type != "prometheus" && result.Type != "" { + zap.S().Warnf("Skipping panel %d as it is not prometheus", idx) + continue + } + + if !stringOk && !structOk { + zap.S().Warnf("Didn't recognize source, skipping") + continue + } + + // Create a panel from "gridPos" + + if idx%3 == 0 { + row++ + } + toReturn.Layout = append( + toReturn.Layout, + model.Layout{ + X: idx % 3 * 4, + Y: row * 3, + W: 4, + H: 3, + I: strconv.Itoa(idx), + }, + ) + + toReturn.Widgets = append(toReturn.Widgets, *widgetFromPanel(panel, idx, toReturn.Variables)) + idx++ + } + return toReturn +} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 1d648f4651..18d2743924 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -27,6 +27,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/dao" am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" + signozio "go.signoz.io/signoz/pkg/query-service/integrations/signozio" "go.signoz.io/signoz/pkg/query-service/interfaces" "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/rules" @@ -339,6 +340,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/dashboards", ViewAccess(aH.getDashboards)).Methods(http.MethodGet) router.HandleFunc("/api/v1/dashboards", EditAccess(aH.createDashboards)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/dashboards/grafana", EditAccess(aH.createDashboardsTransform)).Methods(http.MethodPost) router.HandleFunc("/api/v1/dashboards/{uuid}", ViewAccess(aH.getDashboard)).Methods(http.MethodGet) router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.updateDashboard)).Methods(http.MethodPut) router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete) @@ -358,6 +360,8 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/settings/ttl", ViewAccess(aH.getTTL)).Methods(http.MethodGet) router.HandleFunc("/api/v1/version", OpenAccess(aH.getVersion)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/featureFlags", OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/configs", OpenAccess(aH.getConfigs)).Methods(http.MethodGet) router.HandleFunc("/api/v1/getSpanFilters", ViewAccess(aH.getSpanFilters)).Methods(http.MethodPost) router.HandleFunc("/api/v1/getTagFilters", ViewAccess(aH.getTagFilters)).Methods(http.MethodPost) @@ -822,6 +826,40 @@ func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) { } +func (aH *APIHandler) saveAndReturn(w http.ResponseWriter, signozDashboard model.DashboardData) { + toSave := make(map[string]interface{}) + toSave["title"] = signozDashboard.Title + toSave["description"] = signozDashboard.Description + toSave["tags"] = signozDashboard.Tags + toSave["layout"] = signozDashboard.Layout + toSave["widgets"] = signozDashboard.Widgets + toSave["variables"] = signozDashboard.Variables + + dashboard, apiError := dashboards.CreateDashboard(toSave) + if apiError != nil { + RespondError(w, apiError, nil) + return + } + aH.Respond(w, dashboard) + return +} + +func (aH *APIHandler) createDashboardsTransform(w http.ResponseWriter, r *http.Request) { + + defer r.Body.Close() + b, err := ioutil.ReadAll(r.Body) + + var importData model.GrafanaJSON + + err = json.Unmarshal(b, &importData) + if err == nil { + signozDashboard := dashboards.TransformGrafanaJSONToSignoz(importData) + aH.saveAndReturn(w, signozDashboard) + return + } + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error while creating dashboard from grafana json") +} + func (aH *APIHandler) createDashboards(w http.ResponseWriter, r *http.Request) { var postData map[string]interface{} @@ -1422,7 +1460,7 @@ func (aH *APIHandler) getSpanFilters(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) getFilteredSpans(w http.ResponseWriter, r *http.Request) { - query, err := parseFilteredSpansRequest(r) + query, err := parseFilteredSpansRequest(r, aH) if aH.HandleError(w, err, http.StatusBadRequest) { return } @@ -1533,6 +1571,30 @@ func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) { aH.WriteJSON(w, r, map[string]string{"version": version, "ee": "N"}) } +func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { + featureSet := aH.FF().GetFeatureFlags() + aH.Respond(w, featureSet) +} + +func (aH *APIHandler) FF() interfaces.FeatureLookup { + return aH.featureFlags +} + +func (aH *APIHandler) CheckFeature(f string) bool { + err := aH.FF().CheckFeature(f) + return err == nil +} + +func (aH *APIHandler) getConfigs(w http.ResponseWriter, r *http.Request) { + + configs, err := signozio.FetchDynamicConfigs() + if err != nil { + aH.HandleError(w, err, http.StatusInternalServerError) + return + } + aH.Respond(w, configs) +} + // inviteUser is used to invite a user. It is used by an admin api. func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) { req, err := parseInviteRequest(r) diff --git a/pkg/query-service/app/logs/parser.go b/pkg/query-service/app/logs/parser.go index 1ccb0dbc54..a1ab021e33 100644 --- a/pkg/query-service/app/logs/parser.go +++ b/pkg/query-service/app/logs/parser.go @@ -36,7 +36,7 @@ const ( DESC = "desc" ) -var tokenRegex, _ = regexp.Compile(`(?i)(and( )*?|or( )*?)?(([\w.-]+ (in|nin) \([^(]+\))|([\w.]+ (gt|lt|gte|lte) (')?[\S]+(')?)|([\w.]+ (contains|ncontains)) '[^']+')`) +var tokenRegex, _ = regexp.Compile(`(?i)(and( )*?|or( )*?)?(([\w.-]+ (in|nin) \([^(]+\))|([\w.]+ (gt|lt|gte|lte) (')?[\S]+(')?)|([\w.]+ (contains|ncontains)) [^\\]?'(.*?[^\\])')`) var operatorRegex, _ = regexp.Compile(`(?i)(?: )(in|nin|gt|lt|gte|lte|contains|ncontains)(?: )`) func ParseLogFilterParams(r *http.Request) (*model.LogsFilterParams, error) { diff --git a/pkg/query-service/app/logs/parser_test.go b/pkg/query-service/app/logs/parser_test.go index 439e323739..f4b75f41e0 100644 --- a/pkg/query-service/app/logs/parser_test.go +++ b/pkg/query-service/app/logs/parser_test.go @@ -29,8 +29,8 @@ var correctQueriesTest = []struct { }, { `contains search with a different attributes`, - `resource contains 'Hello, "World"'`, - []string{`resource ILIKE '%Hello, "World"%' `}, + `resource contains 'Hello, "World" and user\'s'`, + []string{`resource ILIKE '%Hello, "World" and user\'s%' `}, }, { `more than one continas`, diff --git a/pkg/query-service/app/metrics/query_builder.go b/pkg/query-service/app/metrics/query_builder.go index e0a696f509..071a32baa3 100644 --- a/pkg/query-service/app/metrics/query_builder.go +++ b/pkg/query-service/app/metrics/query_builder.go @@ -111,21 +111,21 @@ func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string, fmtVal := FormattedValue(toFormat) switch op { case "eq": - conditions = append(conditions, fmt.Sprintf("labels_object.%s = %s", item.Key, fmtVal)) + conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') = %s", item.Key, fmtVal)) case "neq": - conditions = append(conditions, fmt.Sprintf("labels_object.%s != %s", item.Key, fmtVal)) + conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') != %s", item.Key, fmtVal)) case "in": - conditions = append(conditions, fmt.Sprintf("labels_object.%s IN %s", item.Key, fmtVal)) + conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') IN %s", item.Key, fmtVal)) case "nin": - conditions = append(conditions, fmt.Sprintf("labels_object.%s NOT IN %s", item.Key, fmtVal)) + conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') NOT IN %s", item.Key, fmtVal)) case "like": - conditions = append(conditions, fmt.Sprintf("like(labels_object.%s, %s)", item.Key, fmtVal)) + conditions = append(conditions, fmt.Sprintf("like(JSONExtractString(labels, '%s'), %s)", item.Key, fmtVal)) case "nlike": - conditions = append(conditions, fmt.Sprintf("notLike(labels_object.%s, %s)", item.Key, fmtVal)) + conditions = append(conditions, fmt.Sprintf("notLike(JSONExtractString(labels, '%s'), %s)", item.Key, fmtVal)) case "match": - conditions = append(conditions, fmt.Sprintf("match(labels_object.%s, %s)", item.Key, fmtVal)) + conditions = append(conditions, fmt.Sprintf("match(JSONExtractString(labels, '%s'), %s)", item.Key, fmtVal)) case "nmatch": - conditions = append(conditions, fmt.Sprintf("not match(labels_object.%s, %s)", item.Key, fmtVal)) + conditions = append(conditions, fmt.Sprintf("not match(JSONExtractString(labels, '%s'), %s)", item.Key, fmtVal)) default: return "", fmt.Errorf("unsupported operation") } @@ -138,7 +138,7 @@ func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string, selectLabels = "labels," } else { for _, tag := range groupTags { - selectLabels += fmt.Sprintf(" labels_object.%s as %s,", tag, tag) + selectLabels += fmt.Sprintf(" JSONExtractString(labels, '%s') as %s,", tag, tag) } } diff --git a/pkg/query-service/app/metrics/query_builder_test.go b/pkg/query-service/app/metrics/query_builder_test.go index a9cf780ae4..3b15e1f464 100644 --- a/pkg/query-service/app/metrics/query_builder_test.go +++ b/pkg/query-service/app/metrics/query_builder_test.go @@ -55,9 +55,9 @@ func TestBuildQueryWithFilters(t *testing.T) { queries := PrepareBuilderMetricQueries(q, "table").Queries So(len(queries), ShouldEqual, 1) - So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND labels_object.a != 'b'") + So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'a') != 'b'") So(queries["a"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)") - So(queries["a"], ShouldContainSubstring, "not match(labels_object.code, 'ERROR_*')") + So(queries["a"], ShouldContainSubstring, "not match(JSONExtractString(labels, 'code'), 'ERROR_*')") }) } @@ -89,7 +89,7 @@ func TestBuildQueryWithMultipleQueries(t *testing.T) { } queries := PrepareBuilderMetricQueries(q, "table").Queries So(len(queries), ShouldEqual, 2) - So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND labels_object.in IN ['a','b','c']") + So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']") So(queries["a"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)") }) } @@ -126,7 +126,7 @@ func TestBuildQueryWithMultipleQueriesAndFormula(t *testing.T) { queries := PrepareBuilderMetricQueries(q, "table").Queries So(len(queries), ShouldEqual, 3) So(queries["c"], ShouldContainSubstring, "SELECT ts, a.value / b.value") - So(queries["c"], ShouldContainSubstring, "WHERE metric_name = 'name' AND labels_object.in IN ['a','b','c']") + So(queries["c"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']") So(queries["c"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)") }) } diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index 2fed317973..bfd4042d22 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -255,7 +255,7 @@ func parseSpanFilterRequestBody(r *http.Request) (*model.SpanFilterParams, error return postData, nil } -func parseFilteredSpansRequest(r *http.Request) (*model.GetFilteredSpansParams, error) { +func parseFilteredSpansRequest(r *http.Request, aH *APIHandler) (*model.GetFilteredSpansParams, error) { var postData *model.GetFilteredSpansParams err := json.NewDecoder(r.Body).Decode(&postData) @@ -277,6 +277,20 @@ func parseFilteredSpansRequest(r *http.Request) (*model.GetFilteredSpansParams, postData.Limit = 10 } + if len(postData.Order) != 0 { + if postData.Order != constants.Ascending && postData.Order != constants.Descending { + return nil, errors.New("order param is not in correct format") + } + if postData.OrderParam != constants.Duration && postData.OrderParam != constants.Timestamp { + return nil, errors.New("order param is not in correct format") + } + if postData.OrderParam == constants.Duration && !aH.CheckFeature(constants.DurationSort) { + return nil, model.ErrFeatureUnavailable{Key: constants.DurationSort} + } else if postData.OrderParam == constants.Timestamp && !aH.CheckFeature(constants.TimestampSort) { + return nil, model.ErrFeatureUnavailable{Key: constants.TimestampSort} + } + } + return postData, nil } diff --git a/pkg/query-service/app/parser_test.go b/pkg/query-service/app/parser_test.go index 3e78263696..1ce5ec488b 100644 --- a/pkg/query-service/app/parser_test.go +++ b/pkg/query-service/app/parser_test.go @@ -23,7 +23,7 @@ func TestParseFilterSingleFilter(t *testing.T) { req, _ := http.NewRequest("POST", "", bytes.NewReader(postBody)) res, _ := parseFilterSet(req) query, _ := metrics.BuildMetricsTimeSeriesFilterQuery(res, []string{}, "table", model.NOOP) - So(query, ShouldContainSubstring, "signoz_metrics.time_series_v2 WHERE metric_name = 'table' AND labels_object.namespace = 'a'") + So(query, ShouldContainSubstring, "signoz_metrics.time_series_v2 WHERE metric_name = 'table' AND JSONExtractString(labels, 'namespace') = 'a'") }) } @@ -39,8 +39,8 @@ func TestParseFilterMultipleFilter(t *testing.T) { req, _ := http.NewRequest("POST", "", bytes.NewReader(postBody)) res, _ := parseFilterSet(req) query, _ := metrics.BuildMetricsTimeSeriesFilterQuery(res, []string{}, "table", model.NOOP) - So(query, should.ContainSubstring, "labels_object.host IN ['host-1','host-2']") - So(query, should.ContainSubstring, "labels_object.namespace = 'a'") + So(query, should.ContainSubstring, "JSONExtractString(labels, 'host') IN ['host-1','host-2']") + So(query, should.ContainSubstring, "JSONExtractString(labels, 'namespace') = 'a'") }) } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 2b46ae8fed..cbb8a807fa 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -19,6 +19,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/dao" + "go.signoz.io/signoz/pkg/query-service/featureManager" "go.signoz.io/signoz/pkg/query-service/healthcheck" am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" "go.signoz.io/signoz/pkg/query-service/interfaces" @@ -77,6 +78,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { } localDB.SetMaxOpenConns(10) + + // initiate feature manager + fm := featureManager.StartManager() + readerReady := make(chan bool) var reader interfaces.Reader @@ -98,9 +103,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { telemetry.GetInstance().SetReader(reader) apiHandler, err := NewAPIHandler(APIHandlerOpts{ - Reader: reader, - AppDao: dao.DB(), - RuleManager: rm, + Reader: reader, + AppDao: dao.DB(), + RuleManager: rm, + FeatureFlags: fm, }) if err != nil { return nil, err diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index 6e3af77a5e..d376b068e9 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -13,6 +13,8 @@ const ( DebugHttpPort = "0.0.0.0:6060" // Address to serve http (pprof) ) +var ConfigSignozIo = "https://config.signoz.io/api/v1" + var DEFAULT_TELEMETRY_ANONYMOUS = false func IsTelemetryEnabled() bool { @@ -28,6 +30,9 @@ const TraceTTL = "traces" const MetricsTTL = "metrics" const LogsTTL = "logs" +const DurationSort = "DurationSort" +const TimestampSort = "TimestampSort" + func GetAlertManagerApiPrefix() string { if os.Getenv("ALERTMANAGER_API_PREFIX") != "" { return os.Getenv("ALERTMANAGER_API_PREFIX") @@ -40,6 +45,33 @@ var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/rout var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db") +var DurationSortFeature = GetOrDefaultEnv("DURATION_SORT_FEATURE", "true") + +var TimestampSortFeature = GetOrDefaultEnv("TIMESTAMP_SORT_FEATURE", "true") + +func IsDurationSortFeatureEnabled() bool { + isDurationSortFeatureEnabledStr := DurationSortFeature + isDurationSortFeatureEnabledBool, err := strconv.ParseBool(isDurationSortFeatureEnabledStr) + if err != nil { + return false + } + return isDurationSortFeatureEnabledBool +} + +func IsTimestampSortFeatureEnabled() bool { + isTimestampSortFeatureEnabledStr := TimestampSortFeature + isTimestampSortFeatureEnabledBool, err := strconv.ParseBool(isTimestampSortFeatureEnabledStr) + if err != nil { + return false + } + return isTimestampSortFeatureEnabledBool +} + +var DEFAULT_FEATURE_SET = model.FeatureSet{ + DurationSort: IsDurationSortFeatureEnabled(), + TimestampSort: IsTimestampSortFeatureEnabled(), +} + const ( TraceID = "traceID" ServiceName = "serviceName" diff --git a/pkg/query-service/featureManager/manager.go b/pkg/query-service/featureManager/manager.go new file mode 100644 index 0000000000..1c8c953982 --- /dev/null +++ b/pkg/query-service/featureManager/manager.go @@ -0,0 +1,34 @@ +package featureManager + +import ( + "go.signoz.io/signoz/pkg/query-service/constants" + "go.signoz.io/signoz/pkg/query-service/model" +) + +type FeatureManager struct { + activeFeatures model.FeatureSet +} + +func StartManager() *FeatureManager { + fM := &FeatureManager{ + activeFeatures: constants.DEFAULT_FEATURE_SET, + } + return fM +} + +// CheckFeature will be internally used by backend routines +// for feature gating +func (fm *FeatureManager) CheckFeature(featureKey string) error { + if value, ok := fm.activeFeatures[featureKey]; ok { + if value { + return nil + } + return model.ErrFeatureUnavailable{Key: featureKey} + } + return model.ErrFeatureUnavailable{Key: featureKey} +} + +// GetFeatureFlags returns current active features +func (fm *FeatureManager) GetFeatureFlags() model.FeatureSet { + return fm.activeFeatures +} diff --git a/pkg/query-service/integrations/signozio/dynamic_config.go b/pkg/query-service/integrations/signozio/dynamic_config.go new file mode 100644 index 0000000000..42827b73d5 --- /dev/null +++ b/pkg/query-service/integrations/signozio/dynamic_config.go @@ -0,0 +1,75 @@ +package signozio + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "time" + + "go.signoz.io/signoz/ee/query-service/model" + "go.signoz.io/signoz/pkg/query-service/constants" +) + +var C *Client + +const ( + POST = "POST" + APPLICATION_JSON = "application/json" +) + +type Client struct { + Prefix string +} + +func New() *Client { + return &Client{ + Prefix: constants.ConfigSignozIo, + } +} + +func init() { + C = New() +} + +// FetchDynamicConfigs fetches configs from config server +func FetchDynamicConfigs() (map[string]Config, *model.ApiError) { + + client := http.Client{Timeout: 5 * time.Second} + req, err := http.NewRequest(http.MethodGet, C.Prefix+"/configs", http.NoBody) + if err != nil { + return DefaultConfig, nil + } + req.SetBasicAuth("admin", "SigNoz@adm1n") + httpResponse, err := client.Do(req) + if err != nil { + return DefaultConfig, nil + } + + defer httpResponse.Body.Close() + + if err != nil { + return DefaultConfig, nil + } + + httpBody, err := ioutil.ReadAll(httpResponse.Body) + if err != nil { + return DefaultConfig, nil + } + + // read api request result + result := ConfigResult{} + err = json.Unmarshal(httpBody, &result) + if err != nil { + return DefaultConfig, nil + } + + switch httpResponse.StatusCode { + case 200, 201: + return result.Data, nil + case 400, 401: + return DefaultConfig, nil + default: + return DefaultConfig, nil + } + +} diff --git a/pkg/query-service/integrations/signozio/response.go b/pkg/query-service/integrations/signozio/response.go new file mode 100644 index 0000000000..8440346ec4 --- /dev/null +++ b/pkg/query-service/integrations/signozio/response.go @@ -0,0 +1,54 @@ +package signozio + +type status string + +type ConfigResult struct { + Status status `json:"status"` + Data map[string]Config `json:"data,omitempty"` + ErrorType string `json:"errorType,omitempty"` + Error string `json:"error,omitempty"` +} + +type Config struct { + Enabled bool `json:"enabled"` + FrontendPositionId string `json:"frontendPositionId"` + Components []ComponentProps `json:"components"` +} + +type ComponentProps struct { + Text string `json:"text"` + Position int `json:"position"` + DarkIcon string `json:"darkIcon"` + LightIcon string `json:"lightIcon"` + Href string `json:"href"` +} + +var DefaultConfig = map[string]Config{ + "helpConfig": { + Enabled: true, + FrontendPositionId: "tooltip", + Components: []ComponentProps{ + { + Text: "How to use SigNoz in production", + Position: 1, + LightIcon: "RiseOutlined", + DarkIcon: "RiseOutlined", + Href: "https://signoz.io/docs/production-readiness", + }, + { + Text: "Create an issue in GitHub", + Position: 2, + LightIcon: "GithubFilled", + DarkIcon: "GithubOutlined", + Href: "https://github.com/SigNoz/signoz/issues/new/choose", + }, + { + Text: "Read the docs", + Position: 3, + LightIcon: "FileTextFilled", + DarkIcon: "FileTextOutlined", + Href: "https://signoz.io/docs", + }, + }, + }, +} diff --git a/pkg/query-service/model/dashboards.go b/pkg/query-service/model/dashboards.go new file mode 100644 index 0000000000..f897eeb7fb --- /dev/null +++ b/pkg/query-service/model/dashboards.go @@ -0,0 +1,254 @@ +package model + +type Datasource struct { + Type string `json:"type"` + UID string `json:"uid"` +} + +type Panels struct { + Datasource interface{} `json:"datasource"` + Description string `json:"description,omitempty"` + FieldConfig struct { + Defaults struct { + Color struct { + Mode string `json:"mode"` + } `json:"color"` + Max float64 `json:"max"` + Min float64 `json:"min"` + Thresholds struct { + Mode string `json:"mode"` + Steps []struct { + Color string `json:"color"` + Value interface{} `json:"value"` + } `json:"steps"` + } `json:"thresholds"` + Unit string `json:"unit"` + } `json:"defaults"` + Overrides []interface{} `json:"overrides"` + } `json:"fieldConfig,omitempty"` + GridPos struct { + H int `json:"h"` + W int `json:"w"` + X int `json:"x"` + Y int `json:"y"` + } `json:"gridPos"` + ID int `json:"id"` + Links []interface{} `json:"links,omitempty"` + Options struct { + Orientation string `json:"orientation"` + ReduceOptions struct { + Calcs []string `json:"calcs"` + Fields string `json:"fields"` + Values bool `json:"values"` + } `json:"reduceOptions"` + ShowThresholdLabels bool `json:"showThresholdLabels"` + ShowThresholdMarkers bool `json:"showThresholdMarkers"` + } `json:"options,omitempty"` + PluginVersion string `json:"pluginVersion,omitempty"` + Targets []struct { + Datasource interface{} `json:"datasource"` + EditorMode string `json:"editorMode"` + Expr string `json:"expr"` + Hide bool `json:"hide"` + IntervalFactor int `json:"intervalFactor"` + LegendFormat string `json:"legendFormat"` + Range bool `json:"range"` + RefID string `json:"refId"` + Step int `json:"step"` + } `json:"targets"` + Title string `json:"title"` + Type string `json:"type"` + HideTimeOverride bool `json:"hideTimeOverride,omitempty"` + MaxDataPoints int `json:"maxDataPoints,omitempty"` + Collapsed bool `json:"collapsed,omitempty"` + Panels []Panels `json:"panels,omitempty"` +} + +type GrafanaJSON struct { + Inputs []struct { + Name string `json:"name"` + Label string `json:"label"` + Description string `json:"description"` + Type string `json:"type"` + PluginID string `json:"pluginId"` + PluginName string `json:"pluginName"` + } `json:"__inputs"` + Requires []struct { + Type string `json:"type"` + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + } `json:"__requires"` + Annotations struct { + List []struct { + HashKey string `json:"$$hashKey"` + BuiltIn int `json:"builtIn"` + Datasource interface{} `json:"datasource"` + Enable bool `json:"enable"` + Hide bool `json:"hide"` + IconColor string `json:"iconColor"` + Name string `json:"name"` + Target struct { + Limit int `json:"limit"` + MatchAny bool `json:"matchAny"` + Tags []interface{} `json:"tags"` + Type string `json:"type"` + } `json:"target"` + Type string `json:"type"` + } `json:"list"` + } `json:"annotations"` + Editable bool `json:"editable"` + FiscalYearStartMonth int `json:"fiscalYearStartMonth"` + GnetID int `json:"gnetId"` + GraphTooltip int `json:"graphTooltip"` + ID interface{} `json:"id"` + Links []struct { + Icon string `json:"icon"` + Tags []interface{} `json:"tags"` + TargetBlank bool `json:"targetBlank"` + Title string `json:"title"` + Type string `json:"type"` + URL string `json:"url"` + } `json:"links"` + LiveNow bool `json:"liveNow"` + Panels []Panels `json:"panels"` + SchemaVersion int `json:"schemaVersion"` + Style string `json:"style"` + Tags []string `json:"tags"` + Templating struct { + List []struct { + Current struct { + Selected bool `json:"selected"` + Text interface{} `json:"text"` + Value interface{} `json:"value"` + } `json:"current"` + Hide int `json:"hide"` + IncludeAll bool `json:"includeAll"` + Label string `json:"label,omitempty"` + Multi bool `json:"multi"` + Name string `json:"name"` + Options []interface{} `json:"options"` + Query interface{} `json:"query"` + Refresh int `json:"refresh,omitempty"` + Regex string `json:"regex,omitempty"` + SkipURLSync bool `json:"skipUrlSync"` + Type string `json:"type"` + Datasource interface{} `json:"datasource,omitempty"` + Definition string `json:"definition,omitempty"` + Sort int `json:"sort,omitempty"` + TagValuesQuery string `json:"tagValuesQuery,omitempty"` + TagsQuery string `json:"tagsQuery,omitempty"` + UseTags bool `json:"useTags,omitempty"` + } `json:"list"` + } `json:"templating"` + Time struct { + From string `json:"from"` + To string `json:"to"` + } `json:"time"` + Timepicker struct { + RefreshIntervals []string `json:"refresh_intervals"` + TimeOptions []string `json:"time_options"` + } `json:"timepicker"` + Timezone string `json:"timezone"` + Title string `json:"title"` + UID string `json:"uid"` + Version int `json:"version"` + WeekStart string `json:"weekStart"` +} +type Layout struct { + H int `json:"h"` + I string `json:"i"` + Moved bool `json:"moved"` + Static bool `json:"static"` + W int `json:"w"` + X int `json:"x"` + Y int `json:"y"` +} + +type Variable struct { + AllSelected bool `json:"allSelected"` + CustomValue string `json:"customValue"` + Description string `json:"description"` + ModificationUUID string `json:"modificationUUID"` + MultiSelect bool `json:"multiSelect"` + QueryValue string `json:"queryValue"` + SelectedValue string `json:"selectedValue"` + ShowALLOption bool `json:"showALLOption"` + Sort string `json:"sort"` + TextboxValue string `json:"textboxValue"` + Type string `json:"type"` +} + +type Data struct { + Legend string `json:"legend"` + Query string `json:"query"` + QueryData []interface{} `json:"queryData"` +} + +type QueryDataDashboard struct { + Data Data `json:"data"` + Error bool `json:"error"` + ErrorMessage string `json:"errorMessage"` + Loading bool `json:"loading"` +} + +type ClickHouseQueryDashboard struct { + Legend string `json:"legend"` + Name string `json:"name"` + Query string `json:"rawQuery"` + Disabled bool `json:"disabled"` +} + +type QueryBuilder struct { + AggregateOperator interface{} `json:"aggregateOperator"` + Disabled bool `json:"disabled"` + GroupBy []string `json:"groupBy"` + Legend string `json:"legend"` + MetricName string `json:"metricName"` + Name string `json:"name"` + TagFilters TagFilters `json:"tagFilters"` + ReduceTo interface{} `json:"reduceTo"` +} + +type MetricsBuilder struct { + Formulas []string `json:"formulas"` + QueryBuilder []QueryBuilder `json:"queryBuilder"` +} + +type PromQueryDashboard struct { + Query string `json:"query"` + Disabled bool `json:"disabled"` + Name string `json:"name"` + Legend string `json:"legend"` +} + +type Query struct { + ClickHouse []ClickHouseQueryDashboard `json:"clickHouse"` + PromQL []PromQueryDashboard `json:"promQL"` + MetricsBuilder MetricsBuilder `json:"metricsBuilder"` + QueryType int `json:"queryType"` +} + +type Widget struct { + Description string `json:"description"` + ID string `json:"id"` + IsStacked bool `json:"isStacked"` + NullZeroValues string `json:"nullZeroValues"` + Opacity string `json:"opacity"` + PanelTypes string `json:"panelTypes"` + Query Query `json:"query"` + QueryData QueryDataDashboard `json:"queryData"` + TimePreferance string `json:"timePreferance"` + Title string `json:"title"` + YAxisUnit string `json:"yAxisUnit"` + QueryType int `json:"queryType"` +} + +type DashboardData struct { + Description string `json:"description"` + Tags []string `json:"tags"` + Layout []Layout `json:"layout"` + Title string `json:"title"` + Widgets []Widget `json:"widgets"` + Variables map[string]Variable `json:"variables"` +} diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index 793c02b8ab..43f1652093 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -44,7 +44,7 @@ const HEART_BEAT_DURATION = 6 * time.Hour // const HEART_BEAT_DURATION = 10 * time.Second const RATE_LIMIT_CHECK_DURATION = 1 * time.Minute -const RATE_LIMIT_VALUE = 60 +const RATE_LIMIT_VALUE = 10 // const RATE_LIMIT_CHECK_DURATION = 20 * time.Second // const RATE_LIMIT_VALUE = 5 diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index 294fed787d..5944764e38 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -2,7 +2,7 @@ version: "2.4" services: clickhouse: - image: clickhouse/clickhouse-server:22.4.5-alpine + image: clickhouse/clickhouse-server:22.8.8-alpine tty: true volumes: - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml @@ -62,7 +62,7 @@ services: condition: service_healthy otel-collector: - image: signoz/signoz-otel-collector:0.55.3 + image: signoz/signoz-otel-collector:0.63.0 command: ["--config=/etc/otel-collector-config.yaml"] user: root # required for reading docker container logs volumes: @@ -78,7 +78,7 @@ services: condition: service_healthy otel-collector-metrics: - image: signoz/signoz-otel-collector:0.55.3 + image: signoz/signoz-otel-collector:0.63.0 command: ["--config=/etc/otel-collector-metrics-config.yaml"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/pkg/query-service/tests/test-deploy/otel-collector-config.yaml b/pkg/query-service/tests/test-deploy/otel-collector-config.yaml index d11e2793b8..33600b35e6 100644 --- a/pkg/query-service/tests/test-deploy/otel-collector-config.yaml +++ b/pkg/query-service/tests/test-deploy/otel-collector-config.yaml @@ -47,7 +47,7 @@ receivers: # thrift_binary: # endpoint: 0.0.0.0:6832 hostmetrics: - collection_interval: 60s + collection_interval: 30s scrapers: cpu: {} load: {} @@ -55,6 +55,16 @@ receivers: disk: {} filesystem: {} network: {} + prometheus: + config: + global: + scrape_interval: 60s + scrape_configs: + # otel-collector internal metrics + - job_name: otel-collector + static_configs: + - targets: + - otel-collector:8888 processors: batch: @@ -88,7 +98,6 @@ processors: resourcedetection: detectors: [env, system] timeout: 2s - override: false extensions: health_check: @@ -126,8 +135,8 @@ service: receivers: [otlp] processors: [batch] exporters: [clickhousemetricswrite] - metrics/hostmetrics: - receivers: [hostmetrics] + metrics/generic: + receivers: [hostmetrics, prometheus] processors: [resourcedetection, batch] exporters: [clickhousemetricswrite] metrics/spanmetrics: diff --git a/pkg/query-service/tests/test-deploy/otel-collector-metrics-config.yaml b/pkg/query-service/tests/test-deploy/otel-collector-metrics-config.yaml index fdc5830f57..aecad4eaaf 100644 --- a/pkg/query-service/tests/test-deploy/otel-collector-metrics-config.yaml +++ b/pkg/query-service/tests/test-deploy/otel-collector-metrics-config.yaml @@ -6,20 +6,14 @@ receivers: prometheus: config: scrape_configs: - # otel-collector internal metrics - - job_name: "otel-collector" - scrape_interval: 60s - static_configs: - - targets: - - otel-collector:8888 # otel-collector-metrics internal metrics - - job_name: "otel-collector-metrics" + - job_name: otel-collector-metrics scrape_interval: 60s static_configs: - targets: - localhost:8888 # SigNoz span metrics - - job_name: "signozspanmetrics-collector" + - job_name: signozspanmetrics-collector scrape_interval: 60s static_configs: - targets: