diff --git a/.devenv/docker/clickhouse/compose.yaml b/.devenv/docker/clickhouse/compose.yaml
index 3beed9e6bc6..d3a825f46d9 100644
--- a/.devenv/docker/clickhouse/compose.yaml
+++ b/.devenv/docker/clickhouse/compose.yaml
@@ -1,6 +1,6 @@
services:
clickhouse:
- image: clickhouse/clickhouse-server:24.1.2-alpine
+ image: clickhouse/clickhouse-server:25.5.6
container_name: clickhouse
volumes:
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
@@ -23,8 +23,10 @@ services:
retries: 3
depends_on:
- zookeeper
+ environment:
+ - CLICKHOUSE_SKIP_USER_SETUP=1
zookeeper:
- image: bitnami/zookeeper:3.7.1
+ image: signoz/zookeeper:3.7.1
container_name: zookeeper
volumes:
- ${PWD}/fs/tmp/zookeeper:/bitnami/zookeeper
@@ -40,7 +42,7 @@ services:
timeout: 5s
retries: 3
schema-migrator-sync:
- image: signoz/signoz-schema-migrator:v0.111.41
+ image: signoz/signoz-schema-migrator:v0.129.6
container_name: schema-migrator-sync
command:
- sync
@@ -53,7 +55,7 @@ services:
condition: service_healthy
restart: on-failure
schema-migrator-async:
- image: signoz/signoz-schema-migrator:v0.111.41
+ image: signoz/signoz-schema-migrator:v0.129.6
container_name: schema-migrator-async
command:
- async
diff --git a/.devenv/docker/signoz-otel-collector/compose.yaml b/.devenv/docker/signoz-otel-collector/compose.yaml
new file mode 100644
index 00000000000..eac598f6de6
--- /dev/null
+++ b/.devenv/docker/signoz-otel-collector/compose.yaml
@@ -0,0 +1,29 @@
+services:
+ signoz-otel-collector:
+ image: signoz/signoz-otel-collector:v0.129.6
+ container_name: signoz-otel-collector-dev
+ command:
+ - --config=/etc/otel-collector-config.yaml
+ - --feature-gates=-pkg.translator.prometheus.NormalizeName
+ volumes:
+ - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
+ environment:
+ - OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
+ - LOW_CARDINAL_EXCEPTION_GROUPING=false
+ ports:
+ - "4317:4317" # OTLP gRPC receiver
+ - "4318:4318" # OTLP HTTP receiver
+ - "13133:13133" # health check extension
+ healthcheck:
+ test:
+ - CMD
+ - wget
+ - --spider
+ - -q
+ - localhost:13133
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ restart: unless-stopped
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
\ No newline at end of file
diff --git a/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml b/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml
new file mode 100644
index 00000000000..43a888fffb7
--- /dev/null
+++ b/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml
@@ -0,0 +1,96 @@
+receivers:
+ otlp:
+ protocols:
+ grpc:
+ endpoint: 0.0.0.0:4317
+ http:
+ endpoint: 0.0.0.0:4318
+ prometheus:
+ config:
+ global:
+ scrape_interval: 60s
+ scrape_configs:
+ - job_name: otel-collector
+ static_configs:
+ - targets:
+ - localhost:8888
+ labels:
+ job_name: otel-collector
+
+processors:
+ batch:
+ send_batch_size: 10000
+ send_batch_max_size: 11000
+ timeout: 10s
+ resourcedetection:
+ # Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
+ detectors: [env, system]
+ timeout: 2s
+ signozspanmetrics/delta:
+ metrics_exporter: signozclickhousemetrics
+ metrics_flush_interval: 60s
+ latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
+ dimensions_cache_size: 100000
+ aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
+ enable_exp_histogram: true
+ dimensions:
+ - name: service.namespace
+ default: default
+ - name: deployment.environment
+ default: default
+ # This is added to ensure the uniqueness of the timeseries
+ # Otherwise, identical timeseries produced by multiple replicas of
+ # collectors result in incorrect APM metrics
+ - name: signoz.collector.id
+ - name: service.version
+ - name: browser.platform
+ - name: browser.mobile
+ - name: k8s.cluster.name
+ - name: k8s.node.name
+ - name: k8s.namespace.name
+ - name: host.name
+ - name: host.type
+ - name: container.name
+
+extensions:
+ health_check:
+ endpoint: 0.0.0.0:13133
+ pprof:
+ endpoint: 0.0.0.0:1777
+
+exporters:
+ clickhousetraces:
+ datasource: tcp://host.docker.internal:9000/signoz_traces
+ low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
+ use_new_schema: true
+ signozclickhousemetrics:
+ dsn: tcp://host.docker.internal:9000/signoz_metrics
+ clickhouselogsexporter:
+ dsn: tcp://host.docker.internal:9000/signoz_logs
+ timeout: 10s
+ use_new_schema: true
+
+service:
+ telemetry:
+ logs:
+ encoding: json
+ extensions:
+ - health_check
+ - pprof
+ pipelines:
+ traces:
+ receivers: [otlp]
+ processors: [signozspanmetrics/delta, batch]
+ exporters: [clickhousetraces]
+ metrics:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [signozclickhousemetrics]
+ metrics/prometheus:
+ receivers: [prometheus]
+ processors: [batch]
+ exporters: [signozclickhousemetrics]
+ logs:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [clickhouselogsexporter]
\ No newline at end of file
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 38fc2a7821f..b525276f814 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -5,10 +5,83 @@
/frontend/ @SigNoz/frontend @YounixM
/frontend/src/container/MetricsApplication @srikanthccv
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
+
+# Dashboard, Alert, Metrics, Service Map, Services
+/frontend/src/container/ListOfDashboard/ @srikanthccv
+/frontend/src/container/NewDashboard/ @srikanthccv
+/frontend/src/pages/DashboardsListPage/ @srikanthccv
+/frontend/src/pages/DashboardWidget/ @srikanthccv
+/frontend/src/pages/NewDashboard/ @srikanthccv
+/frontend/src/providers/Dashboard/ @srikanthccv
+
+# Alerts
+/frontend/src/container/AlertHistory/ @srikanthccv
+/frontend/src/container/AllAlertChannels/ @srikanthccv
+/frontend/src/container/AnomalyAlertEvaluationView/ @srikanthccv
+/frontend/src/container/CreateAlertChannels/ @srikanthccv
+/frontend/src/container/CreateAlertRule/ @srikanthccv
+/frontend/src/container/EditAlertChannels/ @srikanthccv
+/frontend/src/container/FormAlertChannels/ @srikanthccv
+/frontend/src/container/FormAlertRules/ @srikanthccv
+/frontend/src/container/ListAlertRules/ @srikanthccv
+/frontend/src/container/TriggeredAlerts/ @srikanthccv
+/frontend/src/pages/AlertChannelCreate/ @srikanthccv
+/frontend/src/pages/AlertDetails/ @srikanthccv
+/frontend/src/pages/AlertHistory/ @srikanthccv
+/frontend/src/pages/AlertList/ @srikanthccv
+/frontend/src/pages/CreateAlert/ @srikanthccv
+/frontend/src/providers/Alert.tsx @srikanthccv
+
+# Metrics
+/frontend/src/container/MetricsExplorer/ @srikanthccv
+/frontend/src/pages/MetricsApplication/ @srikanthccv
+/frontend/src/pages/MetricsExplorer/ @srikanthccv
+
+# Services and Service Map
+/frontend/src/container/ServiceApplication/ @srikanthccv
+/frontend/src/container/ServiceTable/ @srikanthccv
+/frontend/src/pages/Services/ @srikanthccv
+/frontend/src/pages/ServiceTopLevelOperations/ @srikanthccv
+/frontend/src/container/Home/Services/ @srikanthccv
+
/deploy/ @SigNoz/devops
.github @SigNoz/devops
+
+# Scaffold Owners
/pkg/config/ @grandwizard28
/pkg/errors/ @grandwizard28
/pkg/factory/ @grandwizard28
/pkg/types/ @grandwizard28
+/pkg/valuer/ @grandwizard28
+/cmd/ @grandwizard28
+.golangci.yml @grandwizard28
+
+# Zeus Owners
+/pkg/zeus/ @vikrantgupta25
+/ee/zeus/ @vikrantgupta25
+/pkg/licensing/ @vikrantgupta25
+/ee/licensing/ @vikrantgupta25
+
+# SQL Owners
/pkg/sqlmigration/ @vikrantgupta25
+/ee/sqlmigration/ @vikrantgupta25
+/pkg/sqlschema/ @vikrantgupta25
+/ee/sqlschema/ @vikrantgupta25
+
+# Analytics Owners
+/pkg/analytics/ @vikrantgupta25
+/pkg/statsreporter/ @vikrantgupta25
+
+# Querier Owners
+/pkg/querier/ @srikanthccv
+/pkg/variables/ @srikanthccv
+/pkg/types/querybuildertypes/ @srikanthccv
+/pkg/querybuilder/ @srikanthccv
+/pkg/telemetrylogs/ @srikanthccv
+/pkg/telemetrymetadata/ @srikanthccv
+/pkg/telemetrymetrics/ @srikanthccv
+/pkg/telemetrytraces/ @srikanthccv
+
+# AuthN / AuthZ Owners
+
+/pkg/authz/ @vikrantgupta25 @grandwizard28
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 6260fd59406..0691744c322 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -32,9 +32,7 @@ ex:
> Tag the relevant teams for review:
-- [ ] @SigNoz/frontend
-- [ ] @SigNoz/backend
-- [ ] @SigNoz/devops
+- frontend / backend / devops
---
diff --git a/.github/workflows/build-community.yaml b/.github/workflows/build-community.yaml
index a4f0ba93f15..d8d1df3a623 100644
--- a/.github/workflows/build-community.yaml
+++ b/.github/workflows/build-community.yaml
@@ -62,11 +62,11 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
- GO_VERSION: 1.23
+ GO_VERSION: 1.24
GO_NAME: signoz-community
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
- GO_BUILD_CONTEXT: ./pkg/query-service
+ GO_BUILD_CONTEXT: ./cmd/community
GO_BUILD_FLAGS: >-
-tags timetzdata
-ldflags='-linkmode external -extldflags \"-static\" -s -w
@@ -74,9 +74,10 @@ jobs:
-X github.com/SigNoz/signoz/pkg/version.variant=community
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
- -X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}'
+ -X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
+ -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
- DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch
+ DOCKER_DOCKERFILE_PATH: ./cmd/community/Dockerfile.multi-arch
DOCKER_MANIFEST: true
DOCKER_PROVIDERS: dockerhub
diff --git a/.github/workflows/build-enterprise.yaml b/.github/workflows/build-enterprise.yaml
index 4031abed106..96d239a3877 100644
--- a/.github/workflows/build-enterprise.yaml
+++ b/.github/workflows/build-enterprise.yaml
@@ -67,9 +67,8 @@ jobs:
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
- echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> frontend/.env
- echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> frontend/.env
- echo 'USERPILOT_KEY="${{ secrets.USERPILOT_KEY }}"' >> frontend/.env
+ echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
+ echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:
@@ -94,10 +93,10 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
- GO_VERSION: 1.23
+ GO_VERSION: 1.24
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
- GO_BUILD_CONTEXT: ./ee/query-service
+ GO_BUILD_CONTEXT: ./cmd/enterprise
GO_BUILD_FLAGS: >-
-tags timetzdata
-ldflags='-linkmode external -extldflags \"-static\" -s -w
@@ -109,9 +108,10 @@ jobs:
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1'
+ -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
+ -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
- DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
+ DOCKER_DOCKERFILE_PATH: ./cmd/enterprise/Dockerfile.multi-arch
DOCKER_MANIFEST: true
DOCKER_PROVIDERS: ${{ needs.prepare.outputs.docker_providers }}
diff --git a/.github/workflows/build-staging.yaml b/.github/workflows/build-staging.yaml
index 0effc134928..271a270ac52 100644
--- a/.github/workflows/build-staging.yaml
+++ b/.github/workflows/build-staging.yaml
@@ -66,7 +66,8 @@ jobs:
echo 'CI=1' > frontend/.env
echo 'TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
- echo 'USERPILOT_KEY="${{ secrets.NP_USERPILOT_KEY }}"' >> frontend/.env
+ echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
+ echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:
@@ -91,10 +92,10 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
- GO_VERSION: 1.23
+ GO_VERSION: 1.24
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
- GO_BUILD_CONTEXT: ./ee/query-service
+ GO_BUILD_CONTEXT: ./cmd/enterprise
GO_BUILD_FLAGS: >-
-tags timetzdata
-ldflags='-linkmode external -extldflags \"-static\" -s -w
@@ -106,10 +107,11 @@ jobs:
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1'
+ -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1
+ -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
- DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
+ DOCKER_DOCKERFILE_PATH: ./cmd/enterprise/Dockerfile.multi-arch
DOCKER_MANIFEST: true
DOCKER_PROVIDERS: gcp
staging:
diff --git a/.github/workflows/goci.yaml b/.github/workflows/goci.yaml
index 73956bbf566..bdafb9499ed 100644
--- a/.github/workflows/goci.yaml
+++ b/.github/workflows/goci.yaml
@@ -18,7 +18,7 @@ jobs:
with:
PRIMUS_REF: main
GO_TEST_CONTEXT: ./...
- GO_VERSION: 1.23
+ GO_VERSION: 1.24
fmt:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -27,7 +27,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
- GO_VERSION: 1.23
+ GO_VERSION: 1.24
lint:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -36,7 +36,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
- GO_VERSION: 1.23
+ GO_VERSION: 1.24
deps:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -45,7 +45,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
- GO_VERSION: 1.23
+ GO_VERSION: 1.24
build:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -57,7 +57,7 @@ jobs:
- name: go-install
uses: actions/setup-go@v5
with:
- go-version: "1.23"
+ go-version: "1.24"
- name: qemu-install
uses: docker/setup-qemu-action@v3
- name: aarch64-install
diff --git a/.github/workflows/gor-signoz-community.yaml b/.github/workflows/gor-signoz-community.yaml
index 5fb3ff1cb55..8a3ead31e39 100644
--- a/.github/workflows/gor-signoz-community.yaml
+++ b/.github/workflows/gor-signoz-community.yaml
@@ -36,7 +36,7 @@ jobs:
- ubuntu-latest
- macos-latest
env:
- CONFIG_PATH: pkg/query-service/.goreleaser.yaml
+ CONFIG_PATH: cmd/community/.goreleaser.yaml
runs-on: ${{ matrix.os }}
steps:
- name: checkout
@@ -58,7 +58,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
- go-version: "1.23"
+ go-version: "1.24"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -100,7 +100,7 @@ jobs:
needs: build
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
- WORKDIR: pkg/query-service
+ WORKDIR: cmd/community
steps:
- name: checkout
uses: actions/checkout@v4
@@ -122,7 +122,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
- go-version: "1.23"
+ go-version: "1.24"
# copy the caches from build
- name: get-sha
diff --git a/.github/workflows/gor-signoz.yaml b/.github/workflows/gor-signoz.yaml
index a74f5aa92da..9ac4ab74a5d 100644
--- a/.github/workflows/gor-signoz.yaml
+++ b/.github/workflows/gor-signoz.yaml
@@ -33,9 +33,8 @@ jobs:
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
- echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> .env
- echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> .env
- echo 'USERPILOT_KEY="${{ secrets.USERPILOT_KEY }}"' >> .env
+ echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
+ echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
- name: build-frontend
run: make js-build
- name: upload-frontend-artifact
@@ -51,7 +50,7 @@ jobs:
- ubuntu-latest
- macos-latest
env:
- CONFIG_PATH: ee/query-service/.goreleaser.yaml
+ CONFIG_PATH: cmd/enterprise/.goreleaser.yaml
runs-on: ${{ matrix.os }}
steps:
- name: checkout
@@ -73,7 +72,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
- go-version: "1.23"
+ go-version: "1.24"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -136,7 +135,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
- go-version: "1.23"
+ go-version: "1.24"
# copy the caches from build
- name: get-sha
diff --git a/.github/workflows/integrationci.yaml b/.github/workflows/integrationci.yaml
index 77e032ba42a..35efa27f6e3 100644
--- a/.github/workflows/integrationci.yaml
+++ b/.github/workflows/integrationci.yaml
@@ -15,14 +15,15 @@ jobs:
matrix:
src:
- bootstrap
+ - auth
+ - querier
sqlstore-provider:
- postgres
- sqlite
clickhouse-version:
- - 24.1.2-alpine
- - 24.12-alpine
+ - 25.5.6
schema-migrator-version:
- - v0.111.38
+ - v0.129.6
postgres-version:
- 15
if: |
diff --git a/.github/workflows/prereleaser.yaml b/.github/workflows/prereleaser.yaml
index 9f891dc0280..3bf16b03bc3 100644
--- a/.github/workflows/prereleaser.yaml
+++ b/.github/workflows/prereleaser.yaml
@@ -1,10 +1,6 @@
name: prereleaser
on:
- # schedule every wednesday 6:30 AM UTC (12:00 PM IST)
- schedule:
- - cron: '30 6 * * 3'
-
# allow manual triggering of the workflow by a maintainer
workflow_dispatch:
inputs:
diff --git a/.github/workflows/run-e2e.yaml b/.github/workflows/run-e2e.yaml
new file mode 100644
index 00000000000..3fef8663cc2
--- /dev/null
+++ b/.github/workflows/run-e2e.yaml
@@ -0,0 +1,62 @@
+name: e2eci
+
+on:
+ workflow_dispatch:
+ inputs:
+ userRole:
+ description: "Role of the user (ADMIN, EDITOR, VIEWER)"
+ required: true
+ type: choice
+ options:
+ - ADMIN
+ - EDITOR
+ - VIEWER
+
+jobs:
+ test:
+ name: Run Playwright Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: lts/*
+
+ - name: Mask secrets and input
+ run: |
+ echo "::add-mask::${{ secrets.BASE_URL }}"
+ echo "::add-mask::${{ secrets.LOGIN_USERNAME }}"
+ echo "::add-mask::${{ secrets.LOGIN_PASSWORD }}"
+ echo "::add-mask::${{ github.event.inputs.userRole }}"
+
+ - name: Install dependencies
+ working-directory: frontend
+ run: |
+ npm install -g yarn
+ yarn
+
+ - name: Install Playwright Browsers
+ working-directory: frontend
+ run: yarn playwright install --with-deps
+
+ - name: Run Playwright Tests
+ working-directory: frontend
+ run: |
+ BASE_URL="${{ secrets.BASE_URL }}" \
+ LOGIN_USERNAME="${{ secrets.LOGIN_USERNAME }}" \
+ LOGIN_PASSWORD="${{ secrets.LOGIN_PASSWORD }}" \
+ USER_ROLE="${{ github.event.inputs.userRole }}" \
+ yarn playwright test
+
+ - name: Upload Playwright Report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report
+ path: frontend/playwright-report/
+ retention-days: 30
diff --git a/.gitignore b/.gitignore
index 51076856927..c002fbe276c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,7 @@ e2e/.auth
# go
vendor/
**/main/**
+__debug_bin**
# git-town
.git-branches.toml
@@ -85,6 +86,8 @@ queries.active
.devenv/**/tmp/**
.qodo
+.dev
+
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -227,4 +230,6 @@ poetry.toml
# LSP config files
pyrightconfig.json
-# End of https://www.toptal.com/developers/gitignore/api/python
\ No newline at end of file
+
+# cursor files
+frontend/.cursor/
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 00000000000..00643925fde
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,39 @@
+linters:
+ default: standard
+ enable:
+ - bodyclose
+ - misspell
+ - nilnil
+ - sloglint
+ - depguard
+ - iface
+ - unparam
+ - forbidigo
+
+linters-settings:
+ sloglint:
+ no-mixed-args: true
+ kv-only: true
+ no-global: all
+ context: all
+ static-msg: true
+ msg-style: lowercased
+ key-naming-case: snake
+ depguard:
+ rules:
+ nozap:
+ deny:
+ - pkg: "go.uber.org/zap"
+ desc: "Do not use zap logger. Use slog instead."
+ noerrors:
+ deny:
+ - pkg: "errors"
+ desc: "Do not use errors package. Use github.com/SigNoz/signoz/pkg/errors instead."
+ iface:
+ enable:
+ - identical
+issues:
+ exclude-dirs:
+ - "pkg/query-service"
+ - "ee/query-service"
+ - "scripts/"
diff --git a/ADVOCATE.md b/ADVOCATE.md
new file mode 100644
index 00000000000..96cf65d6b5c
--- /dev/null
+++ b/ADVOCATE.md
@@ -0,0 +1,62 @@
+# SigNoz Community Advocate Program
+
+Our community is filled with passionate developers who love SigNoz and have been helping spread the word about observability across the world. The SigNoz Community Advocate Program is our way of recognizing these incredible community members and creating deeper collaboration opportunities.
+
+## What is the SigNoz Community Advocate Program?
+
+The SigNoz Community Advocate Program celebrates and supports community members who are already passionate about observability and helping fellow developers. If you're someone who loves discussing SigNoz, helping others with their implementations, or sharing knowledge about observability practices, this program is designed with you in mind.
+
+Our advocates are the heart of the SigNoz community, helping other developers succeed with observability and providing valuable insights that help us build better products.
+
+## What Do Advocates Do?
+
+1. **Community Support**
+
+ - Help fellow developers in our Slack community and GitHub Discussions
+ - Answer questions and share solutions
+ - Guide newcomers through SigNoz self-host implementations
+
+2. **Knowledge Sharing**
+
+ - Spread awareness about observability best practices on developer forums
+ - Create content like blog posts, social media posts, and videos
+ - Host local meetups and events in their regions
+
+3. **Product Collaboration**
+
+ - Provide insights on features, changes, and improvements the community needs
+ - Beta test new features and provide early feedback
+ - Help us understand real-world use cases and pain points
+
+## What's In It For You?
+
+**Recognition & Swag**
+
+- Official recognition as a SigNoz advocate
+- Welcome hamper upon joining
+- Exclusive swag box within your first 3 months
+- Feature on our website (with your permission)
+
+**Early Access**
+
+- First look at new features and updates
+- Direct line to the SigNoz team for feedback and suggestions
+- Opportunity to influence product roadmap
+
+**Community Impact**
+
+- Help shape the observability landscape
+- Build your reputation in the developer community
+- Connect with like-minded developers globally
+
+## How Does It Work?
+
+Currently, the SigNoz Community Advocate Program is **invite-only**. We're starting with a small group of passionate community members who have already been making a difference.
+
+We'll be working closely with our first advocates to shape the program details, benefits, and structure based on what works best for everyone involved.
+
+If you're interested in learning more about the program or want to get more involved in the SigNoz community, join our [Slack community](https://signoz-community.slack.com/) and let us know!
+
+---
+
+*The SigNoz Community Advocate Program recognizes and celebrates the amazing community members who are already passionate about helping fellow developers succeed with observability.*
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 42688328e57..8f49cccf606 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -78,3 +78,5 @@ Need assistance? Join our Slack community:
- Set up your [development environment](docs/contributing/development.md)
- Deploy and observe [SigNoz in action with OpenTelemetry Demo Application](docs/otel-demo-docs.md)
+- Explore the [SigNoz Community Advocate Program](ADVOCATE.md), which recognises contributors who support the community, share their expertise, and help shape SigNoz's future.
+- Write [integration tests](docs/contributing/go/integration.md)
diff --git a/LICENSE b/LICENSE
index 2fef891b370..7e1ae4f6bad 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2,7 +2,7 @@ Copyright (c) 2020-present SigNoz Inc.
Portions of this software are licensed as follows:
-* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
+* All content that resides under the "ee/" and the "cmd/enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
* All third party components incorporated into the SigNoz Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
diff --git a/Makefile b/Makefile
index 31b1764b206..415dc82385d 100644
--- a/Makefile
+++ b/Makefile
@@ -20,18 +20,18 @@ GO_BUILD_LDFLAG_LICENSE_SIGNOZ_IO = -X github.com/SigNoz/signoz/ee/zeus.depreca
GO_BUILD_VERSION_LDFLAGS = -X github.com/SigNoz/signoz/pkg/version.version=$(VERSION) -X github.com/SigNoz/signoz/pkg/version.hash=$(COMMIT_SHORT_SHA) -X github.com/SigNoz/signoz/pkg/version.time=$(TIMESTAMP) -X github.com/SigNoz/signoz/pkg/version.branch=$(BRANCH_NAME)
GO_BUILD_ARCHS_COMMUNITY = $(addprefix go-build-community-,$(ARCHS))
-GO_BUILD_CONTEXT_COMMUNITY = $(SRC)/pkg/query-service
+GO_BUILD_CONTEXT_COMMUNITY = $(SRC)/cmd/community
GO_BUILD_LDFLAGS_COMMUNITY = $(GO_BUILD_VERSION_LDFLAGS) -X github.com/SigNoz/signoz/pkg/version.variant=community
GO_BUILD_ARCHS_ENTERPRISE = $(addprefix go-build-enterprise-,$(ARCHS))
GO_BUILD_ARCHS_ENTERPRISE_RACE = $(addprefix go-build-enterprise-race-,$(ARCHS))
-GO_BUILD_CONTEXT_ENTERPRISE = $(SRC)/ee/query-service
+GO_BUILD_CONTEXT_ENTERPRISE = $(SRC)/cmd/enterprise
GO_BUILD_LDFLAGS_ENTERPRISE = $(GO_BUILD_VERSION_LDFLAGS) -X github.com/SigNoz/signoz/pkg/version.variant=enterprise $(GO_BUILD_LDFLAG_ZEUS_URL) $(GO_BUILD_LDFLAG_LICENSE_SIGNOZ_IO)
DOCKER_BUILD_ARCHS_COMMUNITY = $(addprefix docker-build-community-,$(ARCHS))
-DOCKERFILE_COMMUNITY = $(SRC)/pkg/query-service/Dockerfile
+DOCKERFILE_COMMUNITY = $(SRC)/cmd/community/Dockerfile
DOCKER_REGISTRY_COMMUNITY ?= docker.io/signoz/signoz-community
DOCKER_BUILD_ARCHS_ENTERPRISE = $(addprefix docker-build-enterprise-,$(ARCHS))
-DOCKERFILE_ENTERPRISE = $(SRC)/ee/query-service/Dockerfile
+DOCKERFILE_ENTERPRISE = $(SRC)/cmd/enterprise/Dockerfile
DOCKER_REGISTRY_ENTERPRISE ?= docker.io/signoz/signoz
JS_BUILD_CONTEXT = $(SRC)/frontend
@@ -61,6 +61,17 @@ devenv-postgres: ## Run postgres in devenv
@cd .devenv/docker/postgres; \
docker compose -f compose.yaml up -d
+.PHONY: devenv-signoz-otel-collector
+devenv-signoz-otel-collector: ## Run signoz-otel-collector in devenv (requires clickhouse to be running)
+ @cd .devenv/docker/signoz-otel-collector; \
+ docker compose -f compose.yaml up -d
+
+.PHONY: devenv-up
+devenv-up: devenv-clickhouse devenv-signoz-otel-collector ## Start both clickhouse and signoz-otel-collector for local development
+ @echo "Development environment is ready!"
+ @echo " - ClickHouse: http://localhost:8123"
+ @echo " - Signoz OTel Collector: grpc://localhost:4317, http://localhost:4318"
+
##############################################################
# go commands
##############################################################
@@ -74,7 +85,7 @@ go-run-enterprise: ## Runs the enterprise go backend server
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
go run -race \
- $(GO_BUILD_CONTEXT_ENTERPRISE)/main.go \
+ $(GO_BUILD_CONTEXT_ENTERPRISE)/*.go \
--config ./conf/prometheus.yml \
--cluster cluster
@@ -92,7 +103,7 @@ go-run-community: ## Runs the community go backend server
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
go run -race \
- $(GO_BUILD_CONTEXT_COMMUNITY)/main.go \
+ $(GO_BUILD_CONTEXT_COMMUNITY)/*.go server \
--config ./conf/prometheus.yml \
--cluster cluster
diff --git a/README.md b/README.md
index 60f8621703a..b3c4c83d8f0 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,6 @@
All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit signoz.io for the full documentation, tutorials, and guide.
-
@@ -231,6 +230,8 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
- [Shaheer Kochai](https://github.com/ahmadshaheer)
- [Amlan Kumar Nandy](https://github.com/amlannandy)
- [Sahil Khan](https://github.com/sawhil)
+- [Aditya Singh](https://github.com/aks07)
+- [Abhi Kumar](https://github.com/ahrefabhi)
#### DevOps
diff --git a/pkg/query-service/.goreleaser.yaml b/cmd/community/.goreleaser.yaml
similarity index 93%
rename from pkg/query-service/.goreleaser.yaml
rename to cmd/community/.goreleaser.yaml
index 8bd46312059..74d165d7f4f 100644
--- a/pkg/query-service/.goreleaser.yaml
+++ b/cmd/community/.goreleaser.yaml
@@ -11,7 +11,7 @@ before:
builds:
- id: signoz
binary: bin/signoz
- main: pkg/query-service/main.go
+ main: ./cmd/community
env:
- CGO_ENABLED=1
- >-
@@ -35,6 +35,7 @@ builds:
- -X github.com/SigNoz/signoz/pkg/version.hash={{ .ShortCommit }}
- -X github.com/SigNoz/signoz/pkg/version.time={{ .CommitTimestamp }}
- -X github.com/SigNoz/signoz/pkg/version.branch={{ .Branch }}
+ - -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
- >-
{{- if eq .Os "linux" }}-linkmode external -extldflags '-static'{{- end }}
mod_timestamp: "{{ .CommitTimestamp }}"
diff --git a/pkg/query-service/Dockerfile b/cmd/community/Dockerfile
similarity index 73%
rename from pkg/query-service/Dockerfile
rename to cmd/community/Dockerfile
index 043bfb0cd08..22433506f6a 100644
--- a/pkg/query-service/Dockerfile
+++ b/cmd/community/Dockerfile
@@ -11,11 +11,9 @@ RUN apk update && \
COPY ./target/${OS}-${TARGETARCH}/signoz-community /root/signoz
-COPY ./conf/prometheus.yml /root/config/prometheus.yml
COPY ./templates/email /root/templates
COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz
-ENTRYPOINT ["./signoz"]
-CMD ["-config", "/root/config/prometheus.yml"]
+ENTRYPOINT ["./signoz", "server"]
\ No newline at end of file
diff --git a/pkg/query-service/Dockerfile.multi-arch b/cmd/community/Dockerfile.multi-arch
similarity index 76%
rename from pkg/query-service/Dockerfile.multi-arch
rename to cmd/community/Dockerfile.multi-arch
index dcfb738e949..3a6c479a604 100644
--- a/pkg/query-service/Dockerfile.multi-arch
+++ b/cmd/community/Dockerfile.multi-arch
@@ -12,11 +12,9 @@ RUN apk update && \
rm -rf /var/cache/apk/*
COPY ./target/${OS}-${ARCH}/signoz-community /root/signoz-community
-COPY ./conf/prometheus.yml /root/config/prometheus.yml
COPY ./templates/email /root/templates
COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz-community
-ENTRYPOINT ["./signoz-community"]
-CMD ["-config", "/root/config/prometheus.yml"]
+ENTRYPOINT ["./signoz-community", "server"]
diff --git a/cmd/community/main.go b/cmd/community/main.go
new file mode 100644
index 00000000000..e188635734d
--- /dev/null
+++ b/cmd/community/main.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+ "log/slog"
+
+ "github.com/SigNoz/signoz/cmd"
+ "github.com/SigNoz/signoz/pkg/instrumentation"
+)
+
+func main() {
+ // initialize logger for logging in the cmd/ package. This logger is different from the logger used in the application.
+ logger := instrumentation.NewLogger(instrumentation.Config{Logs: instrumentation.LogsConfig{Level: slog.LevelInfo}})
+
+ // register a list of commands to the root command
+ registerServer(cmd.RootCmd, logger)
+
+ cmd.Execute(logger)
+}
diff --git a/cmd/community/server.go b/cmd/community/server.go
new file mode 100644
index 00000000000..a437b450c17
--- /dev/null
+++ b/cmd/community/server.go
@@ -0,0 +1,116 @@
+package main
+
+import (
+ "context"
+ "log/slog"
+ "time"
+
+ "github.com/SigNoz/signoz/cmd"
+ "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
+ "github.com/SigNoz/signoz/pkg/analytics"
+ "github.com/SigNoz/signoz/pkg/factory"
+ "github.com/SigNoz/signoz/pkg/licensing"
+ "github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
+ "github.com/SigNoz/signoz/pkg/modules/organization"
+ "github.com/SigNoz/signoz/pkg/query-service/app"
+ "github.com/SigNoz/signoz/pkg/signoz"
+ "github.com/SigNoz/signoz/pkg/sqlschema"
+ "github.com/SigNoz/signoz/pkg/sqlstore"
+ "github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
+ "github.com/SigNoz/signoz/pkg/types/authtypes"
+ "github.com/SigNoz/signoz/pkg/version"
+ "github.com/SigNoz/signoz/pkg/zeus"
+ "github.com/SigNoz/signoz/pkg/zeus/noopzeus"
+ "github.com/spf13/cobra"
+)
+
+func registerServer(parentCmd *cobra.Command, logger *slog.Logger) {
+ var flags signoz.DeprecatedFlags
+
+ serverCmd := &cobra.Command{
+ Use: "server",
+ Short: "Run the SigNoz server",
+ FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
+ RunE: func(currCmd *cobra.Command, args []string) error {
+ config, err := cmd.NewSigNozConfig(currCmd.Context(), logger, flags)
+ if err != nil {
+ return err
+ }
+
+ return runServer(currCmd.Context(), config, logger)
+ },
+ }
+
+ flags.RegisterFlags(serverCmd)
+ parentCmd.AddCommand(serverCmd)
+}
+
+func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) error {
+ // print the version
+ version.Info.PrettyPrint(config.Version)
+
+ // add enterprise sqlstore factories to the community sqlstore factories
+ sqlstoreFactories := signoz.NewSQLStoreProviderFactories()
+ if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
+ logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err)
+ return err
+ }
+
+ jwt := authtypes.NewJWT(cmd.NewJWTSecret(ctx, logger), 30*time.Minute, 30*24*time.Hour)
+
+ signoz, err := signoz.New(
+ ctx,
+ config,
+ jwt,
+ zeus.Config{},
+ noopzeus.NewProviderFactory(),
+ licensing.Config{},
+ func(_ sqlstore.SQLStore, _ zeus.Zeus, _ organization.Getter, _ analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
+ return nooplicensing.NewFactory()
+ },
+ signoz.NewEmailingProviderFactories(),
+ signoz.NewCacheProviderFactories(),
+ signoz.NewWebProviderFactories(),
+ func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
+ return signoz.NewSQLSchemaProviderFactories(sqlstore)
+ },
+ signoz.NewSQLStoreProviderFactories(),
+ signoz.NewTelemetryStoreProviderFactories(),
+ )
+ if err != nil {
+ logger.ErrorContext(ctx, "failed to create signoz", "error", err)
+ return err
+ }
+
+ server, err := app.NewServer(config, signoz, jwt)
+ if err != nil {
+ logger.ErrorContext(ctx, "failed to create server", "error", err)
+ return err
+ }
+
+ if err := server.Start(ctx); err != nil {
+ logger.ErrorContext(ctx, "failed to start server", "error", err)
+ return err
+ }
+
+ signoz.Start(ctx)
+
+ if err := signoz.Wait(ctx); err != nil {
+ logger.ErrorContext(ctx, "failed to start signoz", "error", err)
+ return err
+ }
+
+ err = server.Stop(ctx)
+ if err != nil {
+ logger.ErrorContext(ctx, "failed to stop server", "error", err)
+ return err
+ }
+
+ err = signoz.Stop(ctx)
+ if err != nil {
+ logger.ErrorContext(ctx, "failed to stop signoz", "error", err)
+ return err
+ }
+
+ return nil
+}
diff --git a/cmd/config.go b/cmd/config.go
new file mode 100644
index 00000000000..206d9b44d4c
--- /dev/null
+++ b/cmd/config.go
@@ -0,0 +1,41 @@
+package cmd
+
+import (
+ "context"
+ "log/slog"
+ "os"
+
+ "github.com/SigNoz/signoz/pkg/config"
+ "github.com/SigNoz/signoz/pkg/config/envprovider"
+ "github.com/SigNoz/signoz/pkg/config/fileprovider"
+ "github.com/SigNoz/signoz/pkg/signoz"
+)
+
+func NewSigNozConfig(ctx context.Context, logger *slog.Logger, flags signoz.DeprecatedFlags) (signoz.Config, error) {
+ config, err := signoz.NewConfig(
+ ctx,
+ logger,
+ config.ResolverConfig{
+ Uris: []string{"env:"},
+ ProviderFactories: []config.ProviderFactory{
+ envprovider.NewFactory(),
+ fileprovider.NewFactory(),
+ },
+ },
+ flags,
+ )
+ if err != nil {
+ return signoz.Config{}, err
+ }
+
+ return config, nil
+}
+
+func NewJWTSecret(ctx context.Context, logger *slog.Logger) string {
+ jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
+ if len(jwtSecret) == 0 {
+ logger.ErrorContext(ctx, "🚨 CRITICAL SECURITY ISSUE: No JWT secret key specified!", "error", "SIGNOZ_JWT_SECRET environment variable is not set. This has dire consequences for the security of the application. Without a JWT secret, user sessions are vulnerable to tampering and unauthorized access. Please set the SIGNOZ_JWT_SECRET environment variable immediately. For more information, please refer to https://github.com/SigNoz/signoz/issues/8400.")
+ }
+
+ return jwtSecret
+}
diff --git a/ee/query-service/.goreleaser.yaml b/cmd/enterprise/.goreleaser.yaml
similarity index 94%
rename from ee/query-service/.goreleaser.yaml
rename to cmd/enterprise/.goreleaser.yaml
index c4bf6cb011e..d5647b2f494 100644
--- a/ee/query-service/.goreleaser.yaml
+++ b/cmd/enterprise/.goreleaser.yaml
@@ -11,7 +11,7 @@ before:
builds:
- id: signoz
binary: bin/signoz
- main: ee/query-service/main.go
+ main: ./cmd/enterprise
env:
- CGO_ENABLED=1
- >-
@@ -39,6 +39,7 @@ builds:
- -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
- -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
+ - -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
- >-
{{- if eq .Os "linux" }}-linkmode external -extldflags '-static'{{- end }}
mod_timestamp: "{{ .CommitTimestamp }}"
diff --git a/ee/query-service/Dockerfile b/cmd/enterprise/Dockerfile
similarity index 72%
rename from ee/query-service/Dockerfile
rename to cmd/enterprise/Dockerfile
index 1cd477222e4..798055afb1f 100644
--- a/ee/query-service/Dockerfile
+++ b/cmd/enterprise/Dockerfile
@@ -11,11 +11,9 @@ RUN apk update && \
COPY ./target/${OS}-${TARGETARCH}/signoz /root/signoz
-COPY ./conf/prometheus.yml /root/config/prometheus.yml
COPY ./templates/email /root/templates
COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz
-ENTRYPOINT ["./signoz"]
-CMD ["-config", "/root/config/prometheus.yml"]
\ No newline at end of file
+ENTRYPOINT ["./signoz", "server"]
diff --git a/ee/query-service/Dockerfile.integration b/cmd/enterprise/Dockerfile.integration
similarity index 71%
rename from ee/query-service/Dockerfile.integration
rename to cmd/enterprise/Dockerfile.integration
index 40a76a5bbca..7e3b19e9d13 100644
--- a/ee/query-service/Dockerfile.integration
+++ b/cmd/enterprise/Dockerfile.integration
@@ -1,4 +1,12 @@
-FROM golang:1.23-bullseye
+FROM node:18-bullseye AS build
+
+WORKDIR /opt/
+COPY ./frontend/ ./
+ENV NODE_OPTIONS=--max-old-space-size=8192
+RUN CI=1 yarn install
+RUN CI=1 yarn build
+
+FROM golang:1.24-bullseye
ARG OS="linux"
ARG TARGETARCH
@@ -23,6 +31,7 @@ COPY go.mod go.sum ./
RUN go mod download
+COPY ./cmd/ ./cmd/
COPY ./ee/ ./ee/
COPY ./pkg/ ./pkg/
COPY ./templates/email /root/templates
@@ -31,6 +40,8 @@ COPY Makefile Makefile
RUN TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
RUN mv /root/linux-${TARGETARCH}/signoz /root/signoz
+COPY --from=build /opt/build ./web/
+
RUN chmod 755 /root /root/signoz
-ENTRYPOINT ["/root/signoz"]
+ENTRYPOINT ["/root/signoz", "server"]
diff --git a/ee/query-service/Dockerfile.multi-arch b/cmd/enterprise/Dockerfile.multi-arch
similarity index 76%
rename from ee/query-service/Dockerfile.multi-arch
rename to cmd/enterprise/Dockerfile.multi-arch
index 2d3eec3d4b7..776548fcb9d 100644
--- a/ee/query-service/Dockerfile.multi-arch
+++ b/cmd/enterprise/Dockerfile.multi-arch
@@ -12,11 +12,9 @@ RUN apk update && \
rm -rf /var/cache/apk/*
COPY ./target/${OS}-${ARCH}/signoz /root/signoz
-COPY ./conf/prometheus.yml /root/config/prometheus.yml
COPY ./templates/email /root/templates
COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz
-ENTRYPOINT ["./signoz"]
-CMD ["-config", "/root/config/prometheus.yml"]
+ENTRYPOINT ["./signoz", "server"]
diff --git a/cmd/enterprise/main.go b/cmd/enterprise/main.go
new file mode 100644
index 00000000000..e188635734d
--- /dev/null
+++ b/cmd/enterprise/main.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+ "log/slog"
+
+ "github.com/SigNoz/signoz/cmd"
+ "github.com/SigNoz/signoz/pkg/instrumentation"
+)
+
+func main() {
+ // initialize logger for logging in the cmd/ package. This logger is different from the logger used in the application.
+ logger := instrumentation.NewLogger(instrumentation.Config{Logs: instrumentation.LogsConfig{Level: slog.LevelInfo}})
+
+ // register a list of commands to the root command
+ registerServer(cmd.RootCmd, logger)
+
+ cmd.Execute(logger)
+}
diff --git a/cmd/enterprise/server.go b/cmd/enterprise/server.go
new file mode 100644
index 00000000000..b513e9a744b
--- /dev/null
+++ b/cmd/enterprise/server.go
@@ -0,0 +1,124 @@
+package main
+
+import (
+ "context"
+ "log/slog"
+ "time"
+
+ "github.com/SigNoz/signoz/cmd"
+ enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
+ "github.com/SigNoz/signoz/ee/licensing/httplicensing"
+ enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
+ "github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
+ "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
+ enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
+ "github.com/SigNoz/signoz/ee/zeus/httpzeus"
+ "github.com/SigNoz/signoz/pkg/analytics"
+ "github.com/SigNoz/signoz/pkg/factory"
+ "github.com/SigNoz/signoz/pkg/licensing"
+ "github.com/SigNoz/signoz/pkg/modules/organization"
+ "github.com/SigNoz/signoz/pkg/signoz"
+ "github.com/SigNoz/signoz/pkg/sqlschema"
+ "github.com/SigNoz/signoz/pkg/sqlstore"
+ "github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
+ "github.com/SigNoz/signoz/pkg/types/authtypes"
+ "github.com/SigNoz/signoz/pkg/version"
+ "github.com/SigNoz/signoz/pkg/zeus"
+ "github.com/spf13/cobra"
+)
+
+func registerServer(parentCmd *cobra.Command, logger *slog.Logger) {
+ var flags signoz.DeprecatedFlags
+
+ serverCmd := &cobra.Command{
+ Use: "server",
+ Short: "Run the SigNoz server",
+ FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
+ RunE: func(currCmd *cobra.Command, args []string) error {
+ config, err := cmd.NewSigNozConfig(currCmd.Context(), logger, flags)
+ if err != nil {
+ return err
+ }
+
+ return runServer(currCmd.Context(), config, logger)
+ },
+ }
+
+ flags.RegisterFlags(serverCmd)
+ parentCmd.AddCommand(serverCmd)
+}
+
+func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) error {
+ // print the version
+ version.Info.PrettyPrint(config.Version)
+
+ // add enterprise sqlstore factories to the community sqlstore factories
+ sqlstoreFactories := signoz.NewSQLStoreProviderFactories()
+ if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
+ logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err)
+ return err
+ }
+
+ jwt := authtypes.NewJWT(cmd.NewJWTSecret(ctx, logger), 30*time.Minute, 30*24*time.Hour)
+
+ signoz, err := signoz.New(
+ ctx,
+ config,
+ jwt,
+ enterprisezeus.Config(),
+ httpzeus.NewProviderFactory(),
+ enterpriselicensing.Config(24*time.Hour, 3),
+ func(sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
+ return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
+ },
+ signoz.NewEmailingProviderFactories(),
+ signoz.NewCacheProviderFactories(),
+ signoz.NewWebProviderFactories(),
+ func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
+ existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
+ if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {
+ panic(err)
+ }
+
+ return existingFactories
+ },
+ sqlstoreFactories,
+ signoz.NewTelemetryStoreProviderFactories(),
+ )
+ if err != nil {
+ logger.ErrorContext(ctx, "failed to create signoz", "error", err)
+ return err
+ }
+
+ server, err := enterpriseapp.NewServer(config, signoz, jwt)
+ if err != nil {
+ logger.ErrorContext(ctx, "failed to create server", "error", err)
+ return err
+ }
+
+ if err := server.Start(ctx); err != nil {
+ logger.ErrorContext(ctx, "failed to start server", "error", err)
+ return err
+ }
+
+ signoz.Start(ctx)
+
+ if err := signoz.Wait(ctx); err != nil {
+ logger.ErrorContext(ctx, "failed to start signoz", "error", err)
+ return err
+ }
+
+ err = server.Stop(ctx)
+ if err != nil {
+ logger.ErrorContext(ctx, "failed to stop server", "error", err)
+ return err
+ }
+
+ err = signoz.Stop(ctx)
+ if err != nil {
+ logger.ErrorContext(ctx, "failed to stop signoz", "error", err)
+ return err
+ }
+
+ return nil
+}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 00000000000..a080b376450
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,33 @@
+package cmd
+
+import (
+ "log/slog"
+ "os"
+
+ "github.com/SigNoz/signoz/pkg/version"
+ "github.com/spf13/cobra"
+ "go.uber.org/zap" //nolint:depguard
+)
+
+var RootCmd = &cobra.Command{
+ Use: "signoz",
+ Short: "OpenTelemetry-Native Logs, Metrics and Traces in a single pane",
+ Version: version.Info.Version(),
+ SilenceUsage: true,
+ SilenceErrors: true,
+ CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
+}
+
+func Execute(logger *slog.Logger) {
+ zapLogger := newZapLogger()
+ zap.ReplaceGlobals(zapLogger)
+ defer func() {
+ _ = zapLogger.Sync()
+ }()
+
+ err := RootCmd.Execute()
+ if err != nil {
+ logger.ErrorContext(RootCmd.Context(), "error running command", "error", err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/zap.go b/cmd/zap.go
new file mode 100644
index 00000000000..4f043eaf6bc
--- /dev/null
+++ b/cmd/zap.go
@@ -0,0 +1,15 @@
+package cmd
+
+import (
+ "go.uber.org/zap" //nolint:depguard
+ "go.uber.org/zap/zapcore" //nolint:depguard
+)
+
+// Deprecated: Use `NewLogger` from `pkg/instrumentation` instead.
+func newZapLogger() *zap.Logger {
+ config := zap.NewProductionConfig()
+ config.EncoderConfig.TimeKey = "timestamp"
+ config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
+ logger, _ := config.Build()
+ return logger
+}
diff --git a/conf/example.yaml b/conf/example.yaml
index 1ee4a2eeeae..d22fa37cab0 100644
--- a/conf/example.yaml
+++ b/conf/example.yaml
@@ -90,6 +90,15 @@ apiserver:
- /api/v1/version
- /
+##################### Querier #####################
+querier:
+ # The TTL for cached query results.
+ cache_ttl: 168h
+ # The interval for recent data that should not be cached.
+ flux_interval: 5m
+ # The maximum number of concurrent queries for missing ranges.
+ max_concurrent_queries: 4
+
##################### TelemetryStore #####################
telemetrystore:
# Maximum number of idle connections in the connection pool.
@@ -103,13 +112,17 @@ telemetrystore:
clickhouse:
# The DSN to use for clickhouse.
dsn: tcp://localhost:9000
+ # The cluster name to use for clickhouse.
+ cluster: cluster
# The query settings for clickhouse.
settings:
max_execution_time: 0
max_execution_time_leaf: 0
timeout_before_checking_execution_speed: 0
max_bytes_to_read: 0
- max_result_rows_for_ch_query: 0
+ max_result_rows: 0
+ ignore_data_skipping_indices: ""
+ secondary_indices_enable_bulk_filtering: false
##################### Prometheus #####################
prometheus:
@@ -124,10 +137,7 @@ prometheus:
##################### Alertmanager #####################
alertmanager:
# Specifies the alertmanager provider to use.
- provider: legacy
- legacy:
- # The API URL (with prefix) of the legacy Alertmanager instance.
- api_url: http://localhost:9093/api
+ provider: signoz
signoz:
# The poll interval for periodically syncing the alertmanager with the config in the store.
poll_interval: 1m
@@ -165,8 +175,71 @@ alertmanager:
# Retention of the notification logs.
retention: 120h
+##################### Emailing #####################
+emailing:
+ # Whether to enable emailing.
+ enabled: false
+ templates:
+ # The directory containing the email templates. This directory should contain a list of files defined at pkg/types/emailtypes/template.go.
+ directory: /opt/signoz/conf/templates/email
+ smtp:
+ # The SMTP server address.
+ address: localhost:25
+ # The email address to use for the SMTP server.
+ from:
+ # The hello message to use for the SMTP server.
+ hello:
+ # The static headers to send with the email.
+ headers: {}
+ auth:
+ # The username to use for the SMTP server.
+ username:
+ # The password to use for the SMTP server.
+ password:
+ # The secret to use for the SMTP server.
+ secret:
+ # The identity to use for the SMTP server.
+ identity:
+ tls:
+ # Whether to enable TLS. It should be false in most cases since the authentication mechanism should use the STARTTLS extension instead.
+ enabled: false
+ # Whether to skip TLS verification.
+ insecure_skip_verify: false
+ # The path to the CA file.
+ ca_file_path:
+ # The path to the key file.
+ key_file_path:
+ # The path to the certificate file.
+ cert_file_path:
+
+##################### Sharder (experimental) #####################
+sharder:
+ # Specifies the sharder provider to use.
+ provider: noop
+ single:
+ # The org id to which this instance belongs to.
+ org_id: org_id
##################### Analytics #####################
analytics:
# Whether to enable analytics.
enabled: false
+ segment:
+ # The key to use for segment.
+ key: ""
+
+##################### StatsReporter #####################
+statsreporter:
+ # Whether to enable stats reporter. This is used to provide valuable insights to the SigNoz team. It does not collect any sensitive/PII data.
+ enabled: true
+ # The interval at which the stats are collected.
+ interval: 6h
+ collect:
+ # Whether to collect identities and traits (emails).
+ identities: true
+
+
+##################### Gateway (License only) #####################
+gateway:
+ # The URL of the gateway's api.
+ url: http://localhost:8080
diff --git a/deploy/docker-swarm/docker-compose.ha.yaml b/deploy/docker-swarm/docker-compose.ha.yaml
index a29ac8e94f7..56537d57db3 100644
--- a/deploy/docker-swarm/docker-compose.ha.yaml
+++ b/deploy/docker-swarm/docker-compose.ha.yaml
@@ -11,7 +11,7 @@ x-common: &common
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
- image: clickhouse/clickhouse-server:24.1.2-alpine
+ image: clickhouse/clickhouse-server:25.5.6
tty: true
deploy:
labels:
@@ -37,9 +37,11 @@ x-clickhouse-defaults: &clickhouse-defaults
nofile:
soft: 262144
hard: 262144
+ environment:
+ - CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
- image: bitnami/zookeeper:3.7.1
+ image: signoz/zookeeper:3.7.1
user: root
deploy:
labels:
@@ -63,7 +65,7 @@ x-db-depend: &db-depend
services:
init-clickhouse:
!!merge <<: *common
- image: clickhouse/clickhouse-server:24.1.2-alpine
+ image: clickhouse/clickhouse-server:25.5.6
command:
- bash
- -c
@@ -174,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
- image: signoz/signoz:v0.84.1
+ image: signoz/signoz:v0.96.1
command:
- --config=/root/config/prometheus.yml
ports:
@@ -194,6 +196,7 @@ services:
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-swarm
- SIGNOZ_JWT_SECRET=secret
+ - DOT_METRICS_ENABLED=true
healthcheck:
test:
- CMD
@@ -206,7 +209,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
- image: signoz/signoz-otel-collector:v0.111.41
+ image: signoz/signoz-otel-collector:v0.129.6
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -230,7 +233,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
- image: signoz/signoz-schema-migrator:v0.111.41
+ image: signoz/signoz-schema-migrator:v0.129.6
deploy:
restart_policy:
condition: on-failure
diff --git a/deploy/docker-swarm/docker-compose.yaml b/deploy/docker-swarm/docker-compose.yaml
index d7f28a70798..46f136b7aa8 100644
--- a/deploy/docker-swarm/docker-compose.yaml
+++ b/deploy/docker-swarm/docker-compose.yaml
@@ -11,7 +11,7 @@ x-common: &common
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
- image: clickhouse/clickhouse-server:24.1.2-alpine
+ image: clickhouse/clickhouse-server:25.5.6
tty: true
deploy:
labels:
@@ -36,9 +36,11 @@ x-clickhouse-defaults: &clickhouse-defaults
nofile:
soft: 262144
hard: 262144
+ environment:
+ - CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
- image: bitnami/zookeeper:3.7.1
+ image: signoz/zookeeper:3.7.1
user: root
deploy:
labels:
@@ -60,7 +62,7 @@ x-db-depend: &db-depend
services:
init-clickhouse:
!!merge <<: *common
- image: clickhouse/clickhouse-server:24.1.2-alpine
+ image: clickhouse/clickhouse-server:25.5.6
command:
- bash
- -c
@@ -100,26 +102,32 @@ services:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
+
+ configs:
+ - source: clickhouse-config
+ target: /etc/clickhouse-server/config.xml
+ - source: clickhouse-users
+ target: /etc/clickhouse-server/users.xml
+ - source: clickhouse-custom-function
+ target: /etc/clickhouse-server/custom-function.xml
+ - source: clickhouse-cluster
+ target: /etc/clickhouse-server/config.d/cluster.xml
volumes:
- - ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- - ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- - ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- - ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- - ../common/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
- image: signoz/signoz:v0.84.1
+ image: signoz/signoz:v0.96.1
command:
- --config=/root/config/prometheus.yml
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
volumes:
- - ../common/signoz/prometheus.yml:/root/config/prometheus.yml
- - ../common/dashboards:/root/config/dashboards
- sqlite:/var/lib/signoz/
+ configs:
+ - source: signoz-prometheus-config
+ target: /root/config/prometheus.yml
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
@@ -129,6 +137,7 @@ services:
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-swarm
+ - DOT_METRICS_ENABLED=true
healthcheck:
test:
- CMD
@@ -141,15 +150,17 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
- image: signoz/signoz-otel-collector:v0.111.41
+ image: signoz/signoz-otel-collector:v0.129.6
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
- --copy-path=/var/tmp/collector-config.yaml
- --feature-gates=-pkg.translator.prometheus.NormalizeName
- volumes:
- - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- - ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
+ configs:
+ - source: otel-collector-config
+ target: /etc/otel-collector-config.yaml
+ - source: otel-manager-config
+ target: /etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
- LOW_CARDINAL_EXCEPTION_GROUPING=false
@@ -165,7 +176,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
- image: signoz/signoz-schema-migrator:v0.111.41
+ image: signoz/signoz-schema-migrator:v0.129.6
deploy:
restart_policy:
condition: on-failure
@@ -186,3 +197,24 @@ volumes:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
+configs:
+ clickhouse-config:
+ file: ../common/clickhouse/config.xml
+ clickhouse-users:
+ file: ../common/clickhouse/users.xml
+ clickhouse-custom-function:
+ file: ../common/clickhouse/custom-function.xml
+ clickhouse-cluster:
+ file: ../common/clickhouse/cluster.xml
+ signoz-prometheus-config:
+ file: ../common/signoz/prometheus.yml
+ # If you have multiple dashboard files, you can list them individually:
+ # dashboard-foo:
+ # file: ../common/dashboards/foo.json
+ # dashboard-bar:
+ # file: ../common/dashboards/bar.json
+
+ otel-collector-config:
+ file: ./otel-collector-config.yaml
+ otel-manager-config:
+ file: ../common/signoz/otel-collector-opamp-config.yaml
diff --git a/deploy/docker-swarm/otel-collector-config.yaml b/deploy/docker-swarm/otel-collector-config.yaml
index 48e28238f13..48baa16b34c 100644
--- a/deploy/docker-swarm/otel-collector-config.yaml
+++ b/deploy/docker-swarm/otel-collector-config.yaml
@@ -26,7 +26,7 @@ processors:
detectors: [env, system]
timeout: 2s
signozspanmetrics/delta:
- metrics_exporter: clickhousemetricswrite, signozclickhousemetrics
+ metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
@@ -60,27 +60,16 @@ exporters:
datasource: tcp://clickhouse:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
- clickhousemetricswrite:
- endpoint: tcp://clickhouse:9000/signoz_metrics
- resource_to_telemetry_conversion:
- enabled: true
- disable_v2: true
- clickhousemetricswrite/prometheus:
- endpoint: tcp://clickhouse:9000/signoz_metrics
- disable_v2: true
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
use_new_schema: true
- # debug: {}
service:
telemetry:
logs:
encoding: json
- metrics:
- address: 0.0.0.0:8888
extensions:
- health_check
- pprof
@@ -92,11 +81,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
- exporters: [clickhousemetricswrite, signozclickhousemetrics]
+ exporters: [signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
- exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
+ exporters: [signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]
diff --git a/deploy/docker/docker-compose.ha.yaml b/deploy/docker/docker-compose.ha.yaml
index 6dfc5f16b2f..2faeed24fef 100644
--- a/deploy/docker/docker-compose.ha.yaml
+++ b/deploy/docker/docker-compose.ha.yaml
@@ -10,7 +10,7 @@ x-common: &common
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
- image: clickhouse/clickhouse-server:24.1.2-alpine
+ image: clickhouse/clickhouse-server:25.5.6
tty: true
labels:
signoz.io/scrape: "true"
@@ -40,9 +40,11 @@ x-clickhouse-defaults: &clickhouse-defaults
nofile:
soft: 262144
hard: 262144
+ environment:
+ - CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
- image: bitnami/zookeeper:3.7.1
+ image: signoz/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
@@ -65,7 +67,7 @@ x-db-depend: &db-depend
services:
init-clickhouse:
!!merge <<: *common
- image: clickhouse/clickhouse-server:24.1.2-alpine
+ image: clickhouse/clickhouse-server:25.5.6
container_name: signoz-init-clickhouse
command:
- bash
@@ -177,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
- image: signoz/signoz:${VERSION:-v0.84.1}
+ image: signoz/signoz:${VERSION:-v0.96.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -197,6 +199,7 @@ services:
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd
+ - DOT_METRICS_ENABLED=true
healthcheck:
test:
- CMD
@@ -210,7 +213,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
- image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.41}
+ image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.6}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -236,7 +239,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
- image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.41}
+ image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.6}
container_name: schema-migrator-sync
command:
- sync
@@ -247,7 +250,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
- image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.41}
+ image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.6}
container_name: schema-migrator-async
command:
- async
diff --git a/deploy/docker/docker-compose.yaml b/deploy/docker/docker-compose.yaml
index 473c7693e39..66e7433b768 100644
--- a/deploy/docker/docker-compose.yaml
+++ b/deploy/docker/docker-compose.yaml
@@ -9,8 +9,7 @@ x-common: &common
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
- # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
- image: clickhouse/clickhouse-server:24.1.2-alpine
+ image: clickhouse/clickhouse-server:25.5.6
tty: true
labels:
signoz.io/scrape: "true"
@@ -36,9 +35,11 @@ x-clickhouse-defaults: &clickhouse-defaults
nofile:
soft: 262144
hard: 262144
+ environment:
+ - CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
- image: bitnami/zookeeper:3.7.1
+ image: signoz/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
@@ -61,7 +62,7 @@ x-db-depend: &db-depend
services:
init-clickhouse:
!!merge <<: *common
- image: clickhouse/clickhouse-server:24.1.2-alpine
+ image: clickhouse/clickhouse-server:25.5.6
container_name: signoz-init-clickhouse
command:
- bash
@@ -110,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
- image: signoz/signoz:${VERSION:-v0.84.1}
+ image: signoz/signoz:${VERSION:-v0.96.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -130,6 +131,7 @@ services:
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd
+ - DOT_METRICS_ENABLED=true
healthcheck:
test:
- CMD
@@ -142,7 +144,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
- image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.41}
+ image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.6}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -164,7 +166,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
- image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.41}
+ image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.6}
container_name: schema-migrator-sync
command:
- sync
@@ -176,7 +178,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
- image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.41}
+ image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.6}
container_name: schema-migrator-async
command:
- async
diff --git a/deploy/docker/otel-collector-config.yaml b/deploy/docker/otel-collector-config.yaml
index 9830a122360..48baa16b34c 100644
--- a/deploy/docker/otel-collector-config.yaml
+++ b/deploy/docker/otel-collector-config.yaml
@@ -26,7 +26,7 @@ processors:
detectors: [env, system]
timeout: 2s
signozspanmetrics/delta:
- metrics_exporter: clickhousemetricswrite, signozclickhousemetrics
+ metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
@@ -60,27 +60,16 @@ exporters:
datasource: tcp://clickhouse:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
- clickhousemetricswrite:
- endpoint: tcp://clickhouse:9000/signoz_metrics
- disable_v2: true
- resource_to_telemetry_conversion:
- enabled: true
- clickhousemetricswrite/prometheus:
- endpoint: tcp://clickhouse:9000/signoz_metrics
- disable_v2: true
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
use_new_schema: true
- # debug: {}
service:
telemetry:
logs:
encoding: json
- metrics:
- address: 0.0.0.0:8888
extensions:
- health_check
- pprof
@@ -92,11 +81,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
- exporters: [clickhousemetricswrite, signozclickhousemetrics]
+ exporters: [signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
- exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
+ exporters: [signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]
diff --git a/docs/contributing/development.md b/docs/contributing/development.md
index abbd2ce45dc..41f51567b1b 100644
--- a/docs/contributing/development.md
+++ b/docs/contributing/development.md
@@ -44,20 +44,35 @@ Before diving in, make sure you have these tools installed:
SigNoz has three main components: Clickhouse, Backend, and Frontend. Let's set them up one by one.
-### 1. Setting up Clickhouse
+### 1. Setting up ClickHouse
-First, we need to get Clickhouse running:
+First, we need to get ClickHouse running:
```bash
make devenv-clickhouse
```
This command:
-- Starts Clickhouse in a single-shard, single-replica cluster
+- Starts ClickHouse in a single-shard, single-replica cluster
- Sets up Zookeeper
- Runs the latest schema migrations
-### 2. Starting the Backend
+### 2. Setting up SigNoz OpenTelemetry Collector
+
+Next, start the OpenTelemetry Collector to receive telemetry data:
+
+```bash
+make devenv-signoz-otel-collector
+```
+
+This command:
+- Starts the SigNoz OpenTelemetry Collector
+- Listens on port 4317 (gRPC) and 4318 (HTTP) for incoming telemetry data
+- Forwards data to ClickHouse for storage
+
+> 💡 **Quick Setup**: Use `make devenv-up` to start both ClickHouse and OTel Collector together
+
+### 3. Starting the Backend
1. Run the backend server:
```bash
@@ -73,19 +88,24 @@ This command:
> 💡 **Tip**: The API server runs at `http://localhost:8080/` by default
-### 3. Setting up the Frontend
+### 4. Setting up the Frontend
-1. Install dependencies:
+1. Navigate to the frontend directory:
+ ```bash
+ cd frontend
+ ```
+
+2. Install dependencies:
```bash
yarn install
```
-2. Create a `.env` file in the `frontend` directory:
+3. Create a `.env` file in this directory:
```env
FRONTEND_API_ENDPOINT=http://localhost:8080
```
-3. Start the development server:
+4. Start the development server:
```bash
yarn dev
```
@@ -93,3 +113,25 @@ This command:
> 💡 **Tip**: `yarn dev` will automatically rebuild when you make changes to the code
Now you're all set to start developing! Happy coding! 🎉
+
+## Verifying Your Setup
+To verify everything is working correctly:
+
+1. **Check ClickHouse**: `curl http://localhost:8123/ping` (should return "Ok.")
+2. **Check OTel Collector**: `curl http://localhost:13133` (should return health status)
+3. **Check Backend**: `curl http://localhost:8080/api/v1/health` (should return `{"status":"ok"}`)
+4. **Check Frontend**: Open `http://localhost:3301` in your browser
+
+## How to send test data?
+
+You can now send telemetry data to your local SigNoz instance:
+
+- **OTLP gRPC**: `localhost:4317`
+- **OTLP HTTP**: `localhost:4318`
+
+For example, using `curl` to send a test trace:
+```bash
+curl -X POST http://localhost:4318/v1/traces \
+ -H "Content-Type: application/json" \
+ -d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"test-service"}}]},"scopeSpans":[{"spans":[{"traceId":"12345678901234567890123456789012","spanId":"1234567890123456","name":"test-span","startTimeUnixNano":"1609459200000000000","endTimeUnixNano":"1609459201000000000"}]}]}]}'
+```
diff --git a/docs/contributing/go/endpoint.md b/docs/contributing/go/endpoint.md
new file mode 100644
index 00000000000..7c17a20b637
--- /dev/null
+++ b/docs/contributing/go/endpoint.md
@@ -0,0 +1,51 @@
+# Endpoint
+
+This guide outlines the recommended approach for designing endpoints, with a focus on entity relationships, RESTful structure, and examples from the codebase.
+
+## How do we design an endpoint?
+
+### Understand the core entities and their relationships
+
+Start with understanding the core entities and their relationships. For example:
+
+- **Organization**: an organization can have multiple users
+
+### Structure Endpoints RESTfully
+
+Endpoints should reflect the resource hierarchy and follow RESTful conventions. Use clear, **pluralized resource names** and versioning. For example:
+
+- `POST /v1/organizations` — Create an organization
+- `GET /v1/organizations/:id` — Get an organization by id
+- `DELETE /v1/organizations/:id` — Delete an organization by id
+- `PUT /v1/organizations/:id` — Update an organization by id
+- `GET /v1/organizations/:id/users` — Get all users in an organization
+- `GET /v1/organizations/me/users` — Get all users in my organization
+
+Think in terms of resource navigation in a file system. For example, to find your organization, you would navigate to the root of the file system and then to the `organizations` directory. To find a user in an organization, you would navigate to the `organizations` directory and then to the `id` directory.
+
+```bash
+v1/
+├── organizations/
+│ └── 123/
+│ └── users/
+```
+
+`me` endpoints are special. They are used to determine the actual id via some auth/external mechanism. For `me` endpoints, think of the `me` directory being symlinked to your organization directory. For example, if you are a part of the organization `123`, the `me` directory will be symlinked to `/v1/organizations/123`:
+
+```bash
+v1/
+├── organizations/
+│ └── me/ -> symlink to /v1/organizations/123
+│ └── users/
+│ └── 123/
+│ └── users/
+```
+
+> 💡 **Note**: There are various ways to structure endpoints. Some prefer to use singular resource names instead of `me`. Others prefer to use singular resource names for all endpoints. We have, however, chosen to standardize our endpoints in the manner described above.
+
+## What should I remember?
+
+- Use clear, **plural resource names**
+- Use `me` endpoints for determining the actual id via some auth mechanism
+
+> 💡 **Note**: When in doubt, diagram the relationships and walk through the user flows as if navigating a file system. This will help you design endpoints that are both logical and user-friendly.
diff --git a/docs/contributing/go/integration.md b/docs/contributing/go/integration.md
new file mode 100644
index 00000000000..980f4f23ec2
--- /dev/null
+++ b/docs/contributing/go/integration.md
@@ -0,0 +1,213 @@
+# Integration Tests
+
+SigNoz uses integration tests to verify that different components work together correctly in a real environment. These tests run against actual services (ClickHouse, PostgreSQL, etc.) to ensure end-to-end functionality.
+
+## How to set up the integration test environment?
+
+### Prerequisites
+
+Before running integration tests, ensure you have the following installed:
+
+- Python 3.13+
+- Poetry (for dependency management)
+- Docker (for containerized services)
+
+### Initial Setup
+
+1. Navigate to the integration tests directory:
+```bash
+cd tests/integration
+```
+
+2. Install dependencies using Poetry:
+```bash
+poetry install --no-root
+```
+
+### Starting the Test Environment
+
+To spin up all the containers necessary for writing integration tests and keep them running:
+
+```bash
+poetry run pytest --basetemp=./tmp/ -vv --reuse src/bootstrap/setup.py::test_setup
+```
+
+This command will:
+- Start all required services (ClickHouse, PostgreSQL, Zookeeper, etc.)
+- Keep containers running due to the `--reuse` flag
+- Verify that the setup is working correctly
+
+### Stopping the Test Environment
+
+When you're done writing integration tests, clean up the environment:
+
+```bash
+poetry run pytest --basetemp=./tmp/ -vv --teardown -s src/bootstrap/setup.py::test_teardown
+```
+
+This will destroy the running integration test setup and clean up resources.
+
+## Understanding the Integration Test Framework
+
+Python and pytest form the foundation of the integration testing framework. Testcontainers are used to spin up disposable integration environments. Wiremock is used to spin up **test doubles** of other services.
+
+- **Why Python/pytest?** It's expressive, low-boilerplate, and has powerful fixture capabilities that make integration testing straightforward. Extensive libraries for HTTP requests, JSON handling, and data analysis (numpy) make it easier to test APIs and verify data
+- **Why testcontainers?** They let us spin up isolated dependencies that match our production environment without complex setup.
+- **Why wiremock?** Well maintained, documented and extensible.
+
+```
+.
+├── conftest.py
+├── fixtures
+│ ├── __init__.py
+│ ├── auth.py
+│ ├── clickhouse.py
+│ ├── fs.py
+│ ├── http.py
+│ ├── migrator.py
+│ ├── network.py
+│ ├── postgres.py
+│ ├── signoz.py
+│ ├── sql.py
+│ ├── sqlite.py
+│ ├── types.py
+│ └── zookeeper.py
+├── poetry.lock
+├── pyproject.toml
+└── src
+ └── bootstrap
+ ├── __init__.py
+ ├── a_database.py
+ ├── b_register.py
+ └── c_license.py
+```
+
+Each test suite follows some important principles:
+
+1. **Organization**: Test suites live under `src/` in self-contained packages. Fixtures (a pytest concept) live inside `fixtures/`.
+2. **Execution Order**: Files are prefixed with `a_`, `b_`, `c_` to ensure sequential execution.
+3. **Time Constraints**: Each suite should complete in under 10 minutes (setup takes ~4 mins).
+
+### Test Suite Design
+
+Test suites should target functional domains or subsystems within SigNoz. When designing a test suite, consider these principles:
+
+- **Functional Cohesion**: Group tests around a specific capability or service boundary
+- **Data Flow**: Follow the path of data through related components
+- **Change Patterns**: Components frequently modified together should be tested together
+
+The exact boundaries for modules are intentionally flexible, allowing teams to define logical groupings based on their specific context and knowledge of the system.
+
+Eg: The **bootstrap** integration test suite validates core system functionality:
+
+- Database initialization
+- Version check
+
+Other test suites can be **pipelines, auth, querier.**
+
+## How to write an integration test?
+
+Now start writing an integration test. Create a new file `src/bootstrap/e_version.py` and paste the following:
+
+```python
+import requests
+
+from fixtures import types
+from fixtures.logger import setup_logger
+
+logger = setup_logger(__name__)
+
+def test_version(signoz: types.SigNoz) -> None:
+ response = requests.get(signoz.self.host_config.get("/api/v1/version"), timeout=2)
+ logger.info(response)
+```
+
+We have written a simple test which calls the `version` endpoint of the container in step 1. In **order to just run this function, run the following command:**
+
+```bash
+poetry run pytest --basetemp=./tmp/ -vv --reuse src/bootstrap/e_version.py::test_version
+```
+
+> Note: The `--reuse` flag is used to reuse the environment if it is already running. Always use this flag when writing and running integration tests. If you don't use this flag, the environment will be destroyed and recreated every time you run the test.
+
+Here's another example of how to write a more comprehensive integration test:
+
+```python
+from http import HTTPStatus
+import requests
+from fixtures import types
+from fixtures.logger import setup_logger
+
+logger = setup_logger(__name__)
+
+def test_user_registration(signoz: types.SigNoz) -> None:
+ """Test user registration functionality."""
+ response = requests.post(
+ signoz.self.host_configs["8080"].get("/api/v1/register"),
+ json={
+ "name": "testuser",
+ "orgId": "",
+ "orgName": "test.org",
+ "email": "test@example.com",
+ "password": "password123Z$",
+ },
+ timeout=2,
+ )
+
+ assert response.status_code == HTTPStatus.OK
+ assert response.json()["setupCompleted"] is True
+```
+
+## How to run integration tests?
+
+### Running All Tests
+
+```bash
+poetry run pytest --basetemp=./tmp/ -vv --reuse src/
+```
+
+### Running Specific Test Categories
+
+```bash
+poetry run pytest --basetemp=./tmp/ -vv --reuse src/
+
+# Run querier tests
+poetry run pytest --basetemp=./tmp/ -vv --reuse src/querier/
+# Run auth tests
+poetry run pytest --basetemp=./tmp/ -vv --reuse src/auth/
+```
+
+### Running Individual Tests
+
+```bash
+poetry run pytest --basetemp=./tmp/ -vv --reuse src//.py::test_name
+
+# Run test_register in file a_register.py in auth suite
+poetry run pytest --basetemp=./tmp/ -vv --reuse src/auth/a_register.py::test_register
+```
+
+## How to configure different options for integration tests?
+
+Tests can be configured using pytest options:
+
+- `--sqlstore-provider` - Choose database provider (default: postgres)
+- `--postgres-version` - PostgreSQL version (default: 15)
+- `--clickhouse-version` - ClickHouse version (default: 25.5.6)
+- `--zookeeper-version` - Zookeeper version (default: 3.7.1)
+
+Example:
+```bash
+poetry run pytest --basetemp=./tmp/ -vv --reuse --sqlstore-provider=postgres --postgres-version=14 src/auth/
+```
+
+
+## What should I remember?
+
+- **Always use the `--reuse` flag** when setting up the environment to keep containers running
+- **Use the `--teardown` flag** when cleaning up to avoid resource leaks
+- **Follow the naming convention** with alphabetical prefixes for test execution order
+- **Use proper timeouts** in HTTP requests to avoid hanging tests
+- **Clean up test data** between tests to avoid interference
+- **Use descriptive test names** that clearly indicate what is being tested
+- **Leverage fixtures** for common setup and authentication
+- **Test both success and failure scenarios** to ensure robust functionality
diff --git a/docs/contributing/go/provider.md b/docs/contributing/go/provider.md
new file mode 100644
index 00000000000..1270dc318eb
--- /dev/null
+++ b/docs/contributing/go/provider.md
@@ -0,0 +1,106 @@
+# Provider
+
+SigNoz is built on the provider pattern, a design approach where code is organized into providers that handle specific application responsibilities. Providers act as adapter components that integrate with external services and deliver required functionality to the application.
+
+> 💡 **Note**: Coming from a DDD background? Providers are similar (not exactly the same) to adapter/infrastructure services.
+
+## How to create a new provider?
+
+To create a new provider, create a directory in the `pkg/` directory named after your provider. The provider package consists of four key components:
+
+- **Interface** (`pkg//.go`): Defines the provider's interface. Other packages should import this interface to use the provider.
+- **Config** (`pkg//config.go`): Contains provider configuration, implementing the `factory.Config` interface from [factory/config.go](/pkg/factory/config.go).
+- **Implementation** (`pkg///provider.go`): Contains the provider implementation, including a `NewProvider` function that returns a `factory.Provider` interface from [factory/provider.go](/pkg/factory/provider.go).
+- **Mock** (`pkg//test.go`): Provides mocks for the provider, typically used by dependent packages for unit testing.
+
+For example, the [prometheus](/pkg/prometheus) provider delivers a prometheus engine to the application:
+
+- `pkg/prometheus/prometheus.go` - Interface definition
+- `pkg/prometheus/config.go` - Configuration
+- `pkg/prometheus/clickhouseprometheus/provider.go` - Clickhouse-powered implementation
+- `pkg/prometheus/prometheustest/provider.go` - Mock implementation
+
+## How to wire it up?
+
+The `pkg/signoz` package contains the inversion of control container responsible for wiring providers. It handles instantiation, configuration, and assembly of providers based on configuration metadata.
+
+> 💡 **Note**: Coming from a Java background? Providers are similar to Spring beans.
+
+Wiring up a provider involves three steps:
+
+1. Wiring up the configuration
+Add your config from `pkg//config.go` to the `pkg/signoz/config.Config` struct and in new factories:
+
+```go
+type Config struct {
+ ...
+ MyProvider myprovider.Config `mapstructure:"myprovider"`
+ ...
+}
+
+func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, ....) (Config, error) {
+ ...
+ configFactories := []factory.ConfigFactory{
+ myprovider.NewConfigFactory(),
+ }
+ ...
+}
+```
+
+2. Wiring up the provider
+Add available provider implementations in `pkg/signoz/provider.go`:
+
+```go
+func NewMyProviderFactories() factory.NamedMap[factory.ProviderFactory[myprovider.MyProvider, myprovider.Config]] {
+ return factory.MustNewNamedMap(
+ myproviderone.NewFactory(),
+ myprovidertwo.NewFactory(),
+ )
+}
+```
+
+3. Instantiate the provider by adding it to the `SigNoz` struct in `pkg/signoz/signoz.go`:
+
+```go
+type SigNoz struct {
+ ...
+ MyProvider myprovider.MyProvider
+ ...
+}
+
+func New(...) (*SigNoz, error) {
+ ...
+ myprovider, err := myproviderone.New(ctx, settings, config.MyProvider, "one/two")
+ if err != nil {
+ return nil, err
+ }
+ ...
+}
+```
+
+## How to use it?
+
+To use a provider, import its interface. For example, to use the prometheus provider, import `pkg/prometheus/prometheus.go`:
+
+```go
+import "github.com/SigNoz/signoz/pkg/prometheus/prometheus"
+
+func CreateSomething(ctx context.Context, prometheus prometheus.Prometheus) {
+ ...
+ prometheus.DoSomething()
+ ...
+}
+```
+
+## Why do we need this?
+
+Like any dependency injection framework, providers decouple the codebase from implementation details. This is especially valuable in SigNoz's large codebase, where we need to swap implementations without changing dependent code. The provider pattern offers several benefits apart from the obvious one of decoupling:
+
+- Configuration is **defined with each provider and centralized in one place**, making it easier to understand and manage through various methods (environment variables, config files, etc.)
+- Provider mocking is **straightforward for unit testing**, with a consistent pattern for locating mocks
+- **Multiple implementations** of the same provider are **supported**, as demonstrated by our sqlstore provider
+
+## What should I remember?
+
+- Use the provider pattern wherever applicable.
+- Always create a provider **irrespective of the number of implementations**. This makes it easier to add new implementations in the future.
diff --git a/docs/otel-demo-docs.md b/docs/otel-demo-docs.md
index af989efbfdc..2c0fc92ec52 100644
--- a/docs/otel-demo-docs.md
+++ b/docs/otel-demo-docs.md
@@ -16,7 +16,7 @@ __Table of Contents__
- [Prerequisites](#prerequisites-1)
- [Install Helm Repo and Charts](#install-helm-repo-and-charts)
- [Start the OpenTelemetry Demo App](#start-the-opentelemetry-demo-app-1)
- - [Moniitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
+ - [Monitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
- [What's next](#whats-next)
diff --git a/ee/anomaly/daily.go b/ee/anomaly/daily.go
new file mode 100644
index 00000000000..7dd93952c33
--- /dev/null
+++ b/ee/anomaly/daily.go
@@ -0,0 +1,34 @@
+package anomaly
+
+import (
+ "context"
+
+ "github.com/SigNoz/signoz/pkg/valuer"
+)
+
+type DailyProvider struct {
+ BaseSeasonalProvider
+}
+
+var _ BaseProvider = (*DailyProvider)(nil)
+
+func (dp *DailyProvider) GetBaseSeasonalProvider() *BaseSeasonalProvider {
+ return &dp.BaseSeasonalProvider
+}
+
+func NewDailyProvider(opts ...GenericProviderOption[*DailyProvider]) *DailyProvider {
+ dp := &DailyProvider{
+ BaseSeasonalProvider: BaseSeasonalProvider{},
+ }
+
+ for _, opt := range opts {
+ opt(dp)
+ }
+
+ return dp
+}
+
+func (p *DailyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *AnomaliesRequest) (*AnomaliesResponse, error) {
+ req.Seasonality = SeasonalityDaily
+ return p.getAnomalies(ctx, orgID, req)
+}
diff --git a/ee/anomaly/hourly.go b/ee/anomaly/hourly.go
new file mode 100644
index 00000000000..95a21bdaa59
--- /dev/null
+++ b/ee/anomaly/hourly.go
@@ -0,0 +1,35 @@
+package anomaly
+
+import (
+ "context"
+
+ "github.com/SigNoz/signoz/pkg/valuer"
+)
+
+type HourlyProvider struct {
+ BaseSeasonalProvider
+}
+
+var _ BaseProvider = (*HourlyProvider)(nil)
+
+func (hp *HourlyProvider) GetBaseSeasonalProvider() *BaseSeasonalProvider {
+ return &hp.BaseSeasonalProvider
+}
+
+// NewHourlyProvider now uses the generic option type
+func NewHourlyProvider(opts ...GenericProviderOption[*HourlyProvider]) *HourlyProvider {
+ hp := &HourlyProvider{
+ BaseSeasonalProvider: BaseSeasonalProvider{},
+ }
+
+ for _, opt := range opts {
+ opt(hp)
+ }
+
+ return hp
+}
+
+func (p *HourlyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *AnomaliesRequest) (*AnomaliesResponse, error) {
+ req.Seasonality = SeasonalityHourly
+ return p.getAnomalies(ctx, orgID, req)
+}
diff --git a/ee/anomaly/params.go b/ee/anomaly/params.go
new file mode 100644
index 00000000000..9a1aa8a71ce
--- /dev/null
+++ b/ee/anomaly/params.go
@@ -0,0 +1,223 @@
+package anomaly
+
+import (
+ "time"
+
+ qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
+ "github.com/SigNoz/signoz/pkg/valuer"
+)
+
+type Seasonality struct{ valuer.String }
+
+var (
+ SeasonalityHourly = Seasonality{valuer.NewString("hourly")}
+ SeasonalityDaily = Seasonality{valuer.NewString("daily")}
+ SeasonalityWeekly = Seasonality{valuer.NewString("weekly")}
+)
+
+var (
+ oneWeekOffset = uint64(24 * 7 * time.Hour.Milliseconds())
+ oneDayOffset = uint64(24 * time.Hour.Milliseconds())
+ oneHourOffset = uint64(time.Hour.Milliseconds())
+ fiveMinOffset = uint64(5 * time.Minute.Milliseconds())
+)
+
+func (s Seasonality) IsValid() bool {
+ switch s {
+ case SeasonalityHourly, SeasonalityDaily, SeasonalityWeekly:
+ return true
+ default:
+ return false
+ }
+}
+
+type AnomaliesRequest struct {
+ Params qbtypes.QueryRangeRequest
+ Seasonality Seasonality
+}
+
+type AnomaliesResponse struct {
+ Results []*qbtypes.TimeSeriesData
+}
+
+// anomalyParams is the params for anomaly detection
+// prediction = avg(past_period_query) + avg(current_season_query) - mean(past_season_query, past2_season_query, past3_season_query)
+//
+// ^ ^
+// | |
+// (rounded value for past peiod) + (seasonal growth)
+//
+// score = abs(value - prediction) / stddev (current_season_query)
+type anomalyQueryParams struct {
+ // CurrentPeriodQuery is the query range params for period user is looking at or eval window
+ // Example: (now-5m, now), (now-30m, now), (now-1h, now)
+ // The results obtained from this query are used to compare with predicted values
+ // and to detect anomalies
+ CurrentPeriodQuery qbtypes.QueryRangeRequest
+ // PastPeriodQuery is the query range params for past period of seasonality
+ // Example: For weekly seasonality, (now-1w-5m, now-1w)
+ // : For daily seasonality, (now-1d-5m, now-1d)
+ // : For hourly seasonality, (now-1h-5m, now-1h)
+ PastPeriodQuery qbtypes.QueryRangeRequest
+ // CurrentSeasonQuery is the query range params for current period (seasonal)
+ // Example: For weekly seasonality, this is the query range params for the (now-1w-5m, now)
+ // : For daily seasonality, this is the query range params for the (now-1d-5m, now)
+ // : For hourly seasonality, this is the query range params for the (now-1h-5m, now)
+ CurrentSeasonQuery qbtypes.QueryRangeRequest
+ // PastSeasonQuery is the query range params for past seasonal period to the current season
+ // Example: For weekly seasonality, this is the query range params for the (now-2w-5m, now-1w)
+ // : For daily seasonality, this is the query range params for the (now-2d-5m, now-1d)
+ // : For hourly seasonality, this is the query range params for the (now-2h-5m, now-1h)
+ PastSeasonQuery qbtypes.QueryRangeRequest
+ // Past2SeasonQuery is the query range params for past 2 seasonal period to the current season
+ // Example: For weekly seasonality, this is the query range params for the (now-3w-5m, now-2w)
+ // : For daily seasonality, this is the query range params for the (now-3d-5m, now-2d)
+ // : For hourly seasonality, this is the query range params for the (now-3h-5m, now-2h)
+ Past2SeasonQuery qbtypes.QueryRangeRequest
+ // Past3SeasonQuery is the query range params for past 3 seasonal period to the current season
+ // Example: For weekly seasonality, this is the query range params for the (now-4w-5m, now-3w)
+ // : For daily seasonality, this is the query range params for the (now-4d-5m, now-3d)
+ // : For hourly seasonality, this is the query range params for the (now-4h-5m, now-3h)
+ Past3SeasonQuery qbtypes.QueryRangeRequest
+}
+
+func prepareAnomalyQueryParams(req qbtypes.QueryRangeRequest, seasonality Seasonality) *anomalyQueryParams {
+ start := req.Start
+ end := req.End
+
+ currentPeriodQuery := qbtypes.QueryRangeRequest{
+ Start: start,
+ End: end,
+ RequestType: qbtypes.RequestTypeTimeSeries,
+ CompositeQuery: req.CompositeQuery,
+ NoCache: false,
+ }
+
+ var pastPeriodStart, pastPeriodEnd uint64
+
+ switch seasonality {
+ // for one week period, we fetch the data from the past week with 5 min offset
+ case SeasonalityWeekly:
+ pastPeriodStart = start - oneWeekOffset - fiveMinOffset
+ pastPeriodEnd = end - oneWeekOffset
+ // for one day period, we fetch the data from the past day with 5 min offset
+ case SeasonalityDaily:
+ pastPeriodStart = start - oneDayOffset - fiveMinOffset
+ pastPeriodEnd = end - oneDayOffset
+ // for one hour period, we fetch the data from the past hour with 5 min offset
+ case SeasonalityHourly:
+ pastPeriodStart = start - oneHourOffset - fiveMinOffset
+ pastPeriodEnd = end - oneHourOffset
+ }
+
+ pastPeriodQuery := qbtypes.QueryRangeRequest{
+ Start: pastPeriodStart,
+ End: pastPeriodEnd,
+ RequestType: qbtypes.RequestTypeTimeSeries,
+ CompositeQuery: req.CompositeQuery,
+ NoCache: false,
+ }
+
+ // seasonality growth trend
+ var currentGrowthPeriodStart, currentGrowthPeriodEnd uint64
+ switch seasonality {
+ case SeasonalityWeekly:
+ currentGrowthPeriodStart = start - oneWeekOffset
+ currentGrowthPeriodEnd = start
+ case SeasonalityDaily:
+ currentGrowthPeriodStart = start - oneDayOffset
+ currentGrowthPeriodEnd = start
+ case SeasonalityHourly:
+ currentGrowthPeriodStart = start - oneHourOffset
+ currentGrowthPeriodEnd = start
+ }
+
+ currentGrowthQuery := qbtypes.QueryRangeRequest{
+ Start: currentGrowthPeriodStart,
+ End: currentGrowthPeriodEnd,
+ RequestType: qbtypes.RequestTypeTimeSeries,
+ CompositeQuery: req.CompositeQuery,
+ NoCache: false,
+ }
+
+ var pastGrowthPeriodStart, pastGrowthPeriodEnd uint64
+ switch seasonality {
+ case SeasonalityWeekly:
+ pastGrowthPeriodStart = start - 2*oneWeekOffset
+ pastGrowthPeriodEnd = start - 1*oneWeekOffset
+ case SeasonalityDaily:
+ pastGrowthPeriodStart = start - 2*oneDayOffset
+ pastGrowthPeriodEnd = start - 1*oneDayOffset
+ case SeasonalityHourly:
+ pastGrowthPeriodStart = start - 2*oneHourOffset
+ pastGrowthPeriodEnd = start - 1*oneHourOffset
+ }
+
+ pastGrowthQuery := qbtypes.QueryRangeRequest{
+ Start: pastGrowthPeriodStart,
+ End: pastGrowthPeriodEnd,
+ RequestType: qbtypes.RequestTypeTimeSeries,
+ CompositeQuery: req.CompositeQuery,
+ NoCache: false,
+ }
+
+ var past2GrowthPeriodStart, past2GrowthPeriodEnd uint64
+ switch seasonality {
+ case SeasonalityWeekly:
+ past2GrowthPeriodStart = start - 3*oneWeekOffset
+ past2GrowthPeriodEnd = start - 2*oneWeekOffset
+ case SeasonalityDaily:
+ past2GrowthPeriodStart = start - 3*oneDayOffset
+ past2GrowthPeriodEnd = start - 2*oneDayOffset
+ case SeasonalityHourly:
+ past2GrowthPeriodStart = start - 3*oneHourOffset
+ past2GrowthPeriodEnd = start - 2*oneHourOffset
+ }
+
+ past2GrowthQuery := qbtypes.QueryRangeRequest{
+ Start: past2GrowthPeriodStart,
+ End: past2GrowthPeriodEnd,
+ RequestType: qbtypes.RequestTypeTimeSeries,
+ CompositeQuery: req.CompositeQuery,
+ NoCache: false,
+ }
+
+ var past3GrowthPeriodStart, past3GrowthPeriodEnd uint64
+ switch seasonality {
+ case SeasonalityWeekly:
+ past3GrowthPeriodStart = start - 4*oneWeekOffset
+ past3GrowthPeriodEnd = start - 3*oneWeekOffset
+ case SeasonalityDaily:
+ past3GrowthPeriodStart = start - 4*oneDayOffset
+ past3GrowthPeriodEnd = start - 3*oneDayOffset
+ case SeasonalityHourly:
+ past3GrowthPeriodStart = start - 4*oneHourOffset
+ past3GrowthPeriodEnd = start - 3*oneHourOffset
+ }
+
+ past3GrowthQuery := qbtypes.QueryRangeRequest{
+ Start: past3GrowthPeriodStart,
+ End: past3GrowthPeriodEnd,
+ RequestType: qbtypes.RequestTypeTimeSeries,
+ CompositeQuery: req.CompositeQuery,
+ NoCache: false,
+ }
+
+ return &anomalyQueryParams{
+ CurrentPeriodQuery: currentPeriodQuery,
+ PastPeriodQuery: pastPeriodQuery,
+ CurrentSeasonQuery: currentGrowthQuery,
+ PastSeasonQuery: pastGrowthQuery,
+ Past2SeasonQuery: past2GrowthQuery,
+ Past3SeasonQuery: past3GrowthQuery,
+ }
+}
+
+type anomalyQueryResults struct {
+ CurrentPeriodResults []*qbtypes.TimeSeriesData
+ PastPeriodResults []*qbtypes.TimeSeriesData
+ CurrentSeasonResults []*qbtypes.TimeSeriesData
+ PastSeasonResults []*qbtypes.TimeSeriesData
+ Past2SeasonResults []*qbtypes.TimeSeriesData
+ Past3SeasonResults []*qbtypes.TimeSeriesData
+}
diff --git a/ee/anomaly/provider.go b/ee/anomaly/provider.go
new file mode 100644
index 00000000000..7d0686006f5
--- /dev/null
+++ b/ee/anomaly/provider.go
@@ -0,0 +1,11 @@
+package anomaly
+
+import (
+ "context"
+
+ "github.com/SigNoz/signoz/pkg/valuer"
+)
+
+type Provider interface {
+ GetAnomalies(ctx context.Context, orgID valuer.UUID, req *AnomaliesRequest) (*AnomaliesResponse, error)
+}
diff --git a/ee/anomaly/seasonal.go b/ee/anomaly/seasonal.go
new file mode 100644
index 00000000000..6636188b6d0
--- /dev/null
+++ b/ee/anomaly/seasonal.go
@@ -0,0 +1,463 @@
+package anomaly
+
+import (
+ "context"
+ "log/slog"
+ "math"
+
+ "github.com/SigNoz/signoz/pkg/querier"
+ "github.com/SigNoz/signoz/pkg/valuer"
+
+ qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
+)
+
+var (
+ // TODO(srikanthccv): make this configurable?
+ movingAvgWindowSize = 7
+)
+
+// BaseProvider is an interface that includes common methods for all provider types
+type BaseProvider interface {
+ GetBaseSeasonalProvider() *BaseSeasonalProvider
+}
+
+// GenericProviderOption is a generic type for provider options
+type GenericProviderOption[T BaseProvider] func(T)
+
+func WithQuerier[T BaseProvider](querier querier.Querier) GenericProviderOption[T] {
+ return func(p T) {
+ p.GetBaseSeasonalProvider().querier = querier
+ }
+}
+
+func WithLogger[T BaseProvider](logger *slog.Logger) GenericProviderOption[T] {
+ return func(p T) {
+ p.GetBaseSeasonalProvider().logger = logger
+ }
+}
+
+type BaseSeasonalProvider struct {
+ querier querier.Querier
+ logger *slog.Logger
+}
+
+func (p *BaseSeasonalProvider) getQueryParams(req *AnomaliesRequest) *anomalyQueryParams {
+ if !req.Seasonality.IsValid() {
+ req.Seasonality = SeasonalityDaily
+ }
+ return prepareAnomalyQueryParams(req.Params, req.Seasonality)
+}
+
+func (p *BaseSeasonalProvider) toTSResults(ctx context.Context, resp *qbtypes.QueryRangeResponse) []*qbtypes.TimeSeriesData {
+
+ tsData := []*qbtypes.TimeSeriesData{}
+
+ if resp == nil {
+ p.logger.InfoContext(ctx, "nil response from query range")
+ return tsData
+ }
+
+ for _, item := range resp.Data.Results {
+ if resultData, ok := item.(*qbtypes.TimeSeriesData); ok {
+ tsData = append(tsData, resultData)
+ }
+ }
+
+ return tsData
+}
+
+func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID, params *anomalyQueryParams) (*anomalyQueryResults, error) {
+ // TODO(srikanthccv): parallelize this?
+ p.logger.InfoContext(ctx, "fetching results for current period", "anomaly_current_period_query", params.CurrentPeriodQuery)
+ currentPeriodResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.CurrentPeriodQuery)
+ if err != nil {
+ return nil, err
+ }
+
+ p.logger.InfoContext(ctx, "fetching results for past period", "anomaly_past_period_query", params.PastPeriodQuery)
+ pastPeriodResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.PastPeriodQuery)
+ if err != nil {
+ return nil, err
+ }
+
+ p.logger.InfoContext(ctx, "fetching results for current season", "anomaly_current_season_query", params.CurrentSeasonQuery)
+ currentSeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.CurrentSeasonQuery)
+ if err != nil {
+ return nil, err
+ }
+
+ p.logger.InfoContext(ctx, "fetching results for past season", "anomaly_past_season_query", params.PastSeasonQuery)
+ pastSeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.PastSeasonQuery)
+ if err != nil {
+ return nil, err
+ }
+
+ p.logger.InfoContext(ctx, "fetching results for past 2 season", "anomaly_past_2season_query", params.Past2SeasonQuery)
+ past2SeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.Past2SeasonQuery)
+ if err != nil {
+ return nil, err
+ }
+
+ p.logger.InfoContext(ctx, "fetching results for past 3 season", "anomaly_past_3season_query", params.Past3SeasonQuery)
+ past3SeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.Past3SeasonQuery)
+ if err != nil {
+ return nil, err
+ }
+
+ return &anomalyQueryResults{
+ CurrentPeriodResults: p.toTSResults(ctx, currentPeriodResults),
+ PastPeriodResults: p.toTSResults(ctx, pastPeriodResults),
+ CurrentSeasonResults: p.toTSResults(ctx, currentSeasonResults),
+ PastSeasonResults: p.toTSResults(ctx, pastSeasonResults),
+ Past2SeasonResults: p.toTSResults(ctx, past2SeasonResults),
+ Past3SeasonResults: p.toTSResults(ctx, past3SeasonResults),
+ }, nil
+}
+
+// getMatchingSeries gets the matching series from the query result
+// for the given series
+func (p *BaseSeasonalProvider) getMatchingSeries(_ context.Context, queryResult *qbtypes.TimeSeriesData, series *qbtypes.TimeSeries) *qbtypes.TimeSeries {
+ if queryResult == nil || len(queryResult.Aggregations) == 0 || len(queryResult.Aggregations[0].Series) == 0 {
+ return nil
+ }
+
+ for _, curr := range queryResult.Aggregations[0].Series {
+ currLabelsKey := qbtypes.GetUniqueSeriesKey(curr.Labels)
+ seriesLabelsKey := qbtypes.GetUniqueSeriesKey(series.Labels)
+ if currLabelsKey == seriesLabelsKey {
+ return curr
+ }
+ }
+ return nil
+}
+
+func (p *BaseSeasonalProvider) getAvg(series *qbtypes.TimeSeries) float64 {
+ if series == nil || len(series.Values) == 0 {
+ return 0
+ }
+ var sum float64
+ for _, smpl := range series.Values {
+ sum += smpl.Value
+ }
+ return sum / float64(len(series.Values))
+}
+
+func (p *BaseSeasonalProvider) getStdDev(series *qbtypes.TimeSeries) float64 {
+ if series == nil || len(series.Values) == 0 {
+ return 0
+ }
+ avg := p.getAvg(series)
+ var sum float64
+ for _, smpl := range series.Values {
+ sum += math.Pow(smpl.Value-avg, 2)
+ }
+ return math.Sqrt(sum / float64(len(series.Values)))
+}
+
+// getMovingAvg gets the moving average for the given series
+// for the given window size and start index
+func (p *BaseSeasonalProvider) getMovingAvg(series *qbtypes.TimeSeries, movingAvgWindowSize, startIdx int) float64 {
+ if series == nil || len(series.Values) == 0 {
+ return 0
+ }
+ if startIdx >= len(series.Values)-movingAvgWindowSize {
+ startIdx = int(math.Max(0, float64(len(series.Values)-movingAvgWindowSize)))
+ }
+ var sum float64
+ points := series.Values[startIdx:]
+ windowSize := int(math.Min(float64(movingAvgWindowSize), float64(len(points))))
+ for i := 0; i < windowSize; i++ {
+ sum += points[i].Value
+ }
+ avg := sum / float64(windowSize)
+ return avg
+}
+
+func (p *BaseSeasonalProvider) getMean(floats ...float64) float64 {
+ if len(floats) == 0 {
+ return 0
+ }
+ var sum float64
+ for _, f := range floats {
+ sum += f
+ }
+ return sum / float64(len(floats))
+}
+
+func (p *BaseSeasonalProvider) getPredictedSeries(
+ ctx context.Context,
+ series, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries,
+) *qbtypes.TimeSeries {
+ predictedSeries := &qbtypes.TimeSeries{
+ Labels: series.Labels,
+ Values: make([]*qbtypes.TimeSeriesValue, 0),
+ }
+
+ // for each point in the series, get the predicted value
+ // the predicted value is the moving average (with window size = 7) of the previous period series
+ // plus the average of the current season series
+ // minus the mean of the past season series, past2 season series and past3 season series
+ for idx, curr := range series.Values {
+ movingAvg := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
+ avg := p.getAvg(currentSeasonSeries)
+ mean := p.getMean(p.getAvg(pastSeasonSeries), p.getAvg(past2SeasonSeries), p.getAvg(past3SeasonSeries))
+ predictedValue := movingAvg + avg - mean
+
+ if predictedValue < 0 {
+ // this should not happen (except when the data has extreme outliers)
+ // we will use the moving avg of the previous period series in this case
+ p.logger.WarnContext(ctx, "predicted value is less than 0 for series", "anomaly_predicted_value", predictedValue, "anomaly_labels", series.Labels)
+ predictedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
+ }
+
+ p.logger.DebugContext(ctx, "predicted value for series",
+ "anomaly_moving_avg", movingAvg,
+ "anomaly_avg", avg,
+ "anomaly_mean", mean,
+ "anomaly_labels", series.Labels,
+ "anomaly_predicted_value", predictedValue,
+ "anomaly_curr", curr.Value,
+ )
+ predictedSeries.Values = append(predictedSeries.Values, &qbtypes.TimeSeriesValue{
+ Timestamp: curr.Timestamp,
+ Value: predictedValue,
+ })
+ }
+
+ return predictedSeries
+}
+
+// getBounds gets the upper and lower bounds for the given series
+// for the given z score threshold
+// moving avg of the previous period series + z score threshold * std dev of the series
+// moving avg of the previous period series - z score threshold * std dev of the series
+func (p *BaseSeasonalProvider) getBounds(
+ series, predictedSeries *qbtypes.TimeSeries,
+ zScoreThreshold float64,
+) (*qbtypes.TimeSeries, *qbtypes.TimeSeries) {
+ upperBoundSeries := &qbtypes.TimeSeries{
+ Labels: series.Labels,
+ Values: make([]*qbtypes.TimeSeriesValue, 0),
+ }
+
+ lowerBoundSeries := &qbtypes.TimeSeries{
+ Labels: series.Labels,
+ Values: make([]*qbtypes.TimeSeriesValue, 0),
+ }
+
+ for idx, curr := range series.Values {
+ upperBound := p.getMovingAvg(predictedSeries, movingAvgWindowSize, idx) + zScoreThreshold*p.getStdDev(series)
+ lowerBound := p.getMovingAvg(predictedSeries, movingAvgWindowSize, idx) - zScoreThreshold*p.getStdDev(series)
+ upperBoundSeries.Values = append(upperBoundSeries.Values, &qbtypes.TimeSeriesValue{
+ Timestamp: curr.Timestamp,
+ Value: upperBound,
+ })
+ lowerBoundSeries.Values = append(lowerBoundSeries.Values, &qbtypes.TimeSeriesValue{
+ Timestamp: curr.Timestamp,
+ Value: math.Max(lowerBound, 0),
+ })
+ }
+
+ return upperBoundSeries, lowerBoundSeries
+}
+
+// getExpectedValue gets the expected value for the given series
+// for the given index
+// prevSeriesAvg + currentSeasonSeriesAvg - mean of past season series, past2 season series and past3 season series
+func (p *BaseSeasonalProvider) getExpectedValue(
+ _, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries, idx int,
+) float64 {
+ prevSeriesAvg := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
+ currentSeasonSeriesAvg := p.getAvg(currentSeasonSeries)
+ pastSeasonSeriesAvg := p.getAvg(pastSeasonSeries)
+ past2SeasonSeriesAvg := p.getAvg(past2SeasonSeries)
+ past3SeasonSeriesAvg := p.getAvg(past3SeasonSeries)
+ return prevSeriesAvg + currentSeasonSeriesAvg - p.getMean(pastSeasonSeriesAvg, past2SeasonSeriesAvg, past3SeasonSeriesAvg)
+}
+
+// getScore gets the anomaly score for the given series
+// for the given index
+// (value - expectedValue) / std dev of the series
+func (p *BaseSeasonalProvider) getScore(
+ series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries, value float64, idx int,
+) float64 {
+ expectedValue := p.getExpectedValue(series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries, idx)
+ if expectedValue < 0 {
+ expectedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
+ }
+ return (value - expectedValue) / p.getStdDev(weekSeries)
+}
+
+// getAnomalyScores gets the anomaly scores for the given series
+// for the given index
+// (value - expectedValue) / std dev of the series
+func (p *BaseSeasonalProvider) getAnomalyScores(
+ series, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries,
+) *qbtypes.TimeSeries {
+ anomalyScoreSeries := &qbtypes.TimeSeries{
+ Labels: series.Labels,
+ Values: make([]*qbtypes.TimeSeriesValue, 0),
+ }
+
+ for idx, curr := range series.Values {
+ anomalyScore := p.getScore(series, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries, curr.Value, idx)
+ anomalyScoreSeries.Values = append(anomalyScoreSeries.Values, &qbtypes.TimeSeriesValue{
+ Timestamp: curr.Timestamp,
+ Value: anomalyScore,
+ })
+ }
+
+ return anomalyScoreSeries
+}
+
+func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, orgID valuer.UUID, req *AnomaliesRequest) (*AnomaliesResponse, error) {
+ anomalyParams := p.getQueryParams(req)
+ anomalyQueryResults, err := p.getResults(ctx, orgID, anomalyParams)
+ if err != nil {
+ return nil, err
+ }
+
+ currentPeriodResults := make(map[string]*qbtypes.TimeSeriesData)
+ for _, result := range anomalyQueryResults.CurrentPeriodResults {
+ currentPeriodResults[result.QueryName] = result
+ }
+
+ pastPeriodResults := make(map[string]*qbtypes.TimeSeriesData)
+ for _, result := range anomalyQueryResults.PastPeriodResults {
+ pastPeriodResults[result.QueryName] = result
+ }
+
+ currentSeasonResults := make(map[string]*qbtypes.TimeSeriesData)
+ for _, result := range anomalyQueryResults.CurrentSeasonResults {
+ currentSeasonResults[result.QueryName] = result
+ }
+
+ pastSeasonResults := make(map[string]*qbtypes.TimeSeriesData)
+ for _, result := range anomalyQueryResults.PastSeasonResults {
+ pastSeasonResults[result.QueryName] = result
+ }
+
+ past2SeasonResults := make(map[string]*qbtypes.TimeSeriesData)
+ for _, result := range anomalyQueryResults.Past2SeasonResults {
+ past2SeasonResults[result.QueryName] = result
+ }
+
+ past3SeasonResults := make(map[string]*qbtypes.TimeSeriesData)
+ for _, result := range anomalyQueryResults.Past3SeasonResults {
+ past3SeasonResults[result.QueryName] = result
+ }
+
+ for _, result := range currentPeriodResults {
+ funcs := req.Params.FuncsForQuery(result.QueryName)
+
+ var zScoreThreshold float64
+ for _, f := range funcs {
+ if f.Name == qbtypes.FunctionNameAnomaly {
+ for _, arg := range f.Args {
+ if arg.Name != "z_score_threshold" {
+ continue
+ }
+ value, ok := arg.Value.(float64)
+ if ok {
+ zScoreThreshold = value
+ } else {
+ p.logger.InfoContext(ctx, "z_score_threshold not provided, defaulting")
+ zScoreThreshold = 3
+ }
+ break
+ }
+ }
+ }
+
+ pastPeriodResult, ok := pastPeriodResults[result.QueryName]
+ if !ok {
+ continue
+ }
+ currentSeasonResult, ok := currentSeasonResults[result.QueryName]
+ if !ok {
+ continue
+ }
+ pastSeasonResult, ok := pastSeasonResults[result.QueryName]
+ if !ok {
+ continue
+ }
+ past2SeasonResult, ok := past2SeasonResults[result.QueryName]
+ if !ok {
+ continue
+ }
+ past3SeasonResult, ok := past3SeasonResults[result.QueryName]
+ if !ok {
+ continue
+ }
+
+ // no data;
+ if len(result.Aggregations) == 0 {
+ continue
+ }
+
+ aggOfInterest := result.Aggregations[0]
+
+ for _, series := range aggOfInterest.Series {
+ stdDev := p.getStdDev(series)
+ p.logger.InfoContext(ctx, "calculated standard deviation for series", "anomaly_std_dev", stdDev, "anomaly_labels", series.Labels)
+
+ pastPeriodSeries := p.getMatchingSeries(ctx, pastPeriodResult, series)
+ currentSeasonSeries := p.getMatchingSeries(ctx, currentSeasonResult, series)
+ pastSeasonSeries := p.getMatchingSeries(ctx, pastSeasonResult, series)
+ past2SeasonSeries := p.getMatchingSeries(ctx, past2SeasonResult, series)
+ past3SeasonSeries := p.getMatchingSeries(ctx, past3SeasonResult, series)
+
+ prevSeriesAvg := p.getAvg(pastPeriodSeries)
+ currentSeasonSeriesAvg := p.getAvg(currentSeasonSeries)
+ pastSeasonSeriesAvg := p.getAvg(pastSeasonSeries)
+ past2SeasonSeriesAvg := p.getAvg(past2SeasonSeries)
+ past3SeasonSeriesAvg := p.getAvg(past3SeasonSeries)
+ p.logger.InfoContext(ctx, "calculated mean for series",
+ "anomaly_prev_series_avg", prevSeriesAvg,
+ "anomaly_current_season_series_avg", currentSeasonSeriesAvg,
+ "anomaly_past_season_series_avg", pastSeasonSeriesAvg,
+ "anomaly_past_2season_series_avg", past2SeasonSeriesAvg,
+ "anomaly_past_3season_series_avg", past3SeasonSeriesAvg,
+ "anomaly_labels", series.Labels,
+ )
+
+ predictedSeries := p.getPredictedSeries(
+ ctx,
+ series,
+ pastPeriodSeries,
+ currentSeasonSeries,
+ pastSeasonSeries,
+ past2SeasonSeries,
+ past3SeasonSeries,
+ )
+ aggOfInterest.PredictedSeries = append(aggOfInterest.PredictedSeries, predictedSeries)
+
+ upperBoundSeries, lowerBoundSeries := p.getBounds(
+ series,
+ predictedSeries,
+ zScoreThreshold,
+ )
+ aggOfInterest.UpperBoundSeries = append(aggOfInterest.UpperBoundSeries, upperBoundSeries)
+ aggOfInterest.LowerBoundSeries = append(aggOfInterest.LowerBoundSeries, lowerBoundSeries)
+
+ anomalyScoreSeries := p.getAnomalyScores(
+ series,
+ pastPeriodSeries,
+ currentSeasonSeries,
+ pastSeasonSeries,
+ past2SeasonSeries,
+ past3SeasonSeries,
+ )
+ aggOfInterest.AnomalyScores = append(aggOfInterest.AnomalyScores, anomalyScoreSeries)
+ }
+ }
+
+ results := make([]*qbtypes.TimeSeriesData, 0, len(currentPeriodResults))
+ for _, result := range currentPeriodResults {
+ results = append(results, result)
+ }
+
+ return &AnomaliesResponse{
+ Results: results,
+ }, nil
+}
diff --git a/ee/anomaly/weekly.go b/ee/anomaly/weekly.go
new file mode 100644
index 00000000000..c3e3f1fa914
--- /dev/null
+++ b/ee/anomaly/weekly.go
@@ -0,0 +1,34 @@
+package anomaly
+
+import (
+ "context"
+
+ "github.com/SigNoz/signoz/pkg/valuer"
+)
+
+type WeeklyProvider struct {
+ BaseSeasonalProvider
+}
+
+var _ BaseProvider = (*WeeklyProvider)(nil)
+
+func (wp *WeeklyProvider) GetBaseSeasonalProvider() *BaseSeasonalProvider {
+ return &wp.BaseSeasonalProvider
+}
+
+func NewWeeklyProvider(opts ...GenericProviderOption[*WeeklyProvider]) *WeeklyProvider {
+ wp := &WeeklyProvider{
+ BaseSeasonalProvider: BaseSeasonalProvider{},
+ }
+
+ for _, opt := range opts {
+ opt(wp)
+ }
+
+ return wp
+}
+
+func (p *WeeklyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *AnomaliesRequest) (*AnomaliesResponse, error) {
+ req.Seasonality = SeasonalityWeekly
+ return p.getAnomalies(ctx, orgID, req)
+}
diff --git a/ee/authz/openfgaschema/base.fga b/ee/authz/openfgaschema/base.fga
new file mode 100644
index 00000000000..e2f1f003d4c
--- /dev/null
+++ b/ee/authz/openfgaschema/base.fga
@@ -0,0 +1,40 @@
+module base
+
+type organisation
+ relations
+ define read: [user, role#assignee]
+ define update: [user, role#assignee]
+
+type user
+ relations
+ define read: [user, role#assignee]
+ define update: [user, role#assignee]
+ define delete: [user, role#assignee]
+
+type anonymous
+
+type role
+ relations
+ define assignee: [user]
+
+ define read: [user, role#assignee]
+ define update: [user, role#assignee]
+ define delete: [user, role#assignee]
+
+type resources
+ relations
+ define create: [user, role#assignee]
+ define list: [user, role#assignee]
+
+type resource
+ relations
+ define read: [user, anonymous, role#assignee]
+ define update: [user, role#assignee]
+ define delete: [user, role#assignee]
+
+ define block: [user, role#assignee]
+
+
+type telemetry
+ relations
+ define read: [user, anonymous, role#assignee]
diff --git a/ee/authz/openfgaschema/schema.go b/ee/authz/openfgaschema/schema.go
new file mode 100644
index 00000000000..605cad0501f
--- /dev/null
+++ b/ee/authz/openfgaschema/schema.go
@@ -0,0 +1,29 @@
+package openfgaschema
+
+import (
+ "context"
+ _ "embed"
+
+ "github.com/SigNoz/signoz/pkg/authz"
+ openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
+)
+
+var (
+ //go:embed base.fga
+ baseDSL string
+)
+
+type schema struct{}
+
+func NewSchema() authz.Schema {
+ return &schema{}
+}
+
+func (schema *schema) Get(ctx context.Context) []openfgapkgtransformer.ModuleFile {
+ return []openfgapkgtransformer.ModuleFile{
+ {
+ Name: "base.fga",
+ Contents: baseDSL,
+ },
+ }
+}
diff --git a/ee/http/middleware/authz.go b/ee/http/middleware/authz.go
new file mode 100644
index 00000000000..f443917c949
--- /dev/null
+++ b/ee/http/middleware/authz.go
@@ -0,0 +1,132 @@
+package middleware
+
+import (
+ "log/slog"
+ "net/http"
+
+ "github.com/SigNoz/signoz/pkg/authz"
+ "github.com/SigNoz/signoz/pkg/http/render"
+ "github.com/SigNoz/signoz/pkg/types/authtypes"
+ "github.com/gorilla/mux"
+)
+
+const (
+ authzDeniedMessage string = "::AUTHZ-DENIED::"
+)
+
+type AuthZ struct {
+ logger *slog.Logger
+ authzService authz.AuthZ
+}
+
+func NewAuthZ(logger *slog.Logger) *AuthZ {
+ if logger == nil {
+ panic("cannot build authz middleware, logger is empty")
+ }
+
+ return &AuthZ{logger: logger}
+}
+
+func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ claims, err := authtypes.ClaimsFromContext(req.Context())
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ if err := claims.IsViewer(); err != nil {
+ middleware.logger.WarnContext(req.Context(), authzDeniedMessage, "claims", claims)
+ render.Error(rw, err)
+ return
+ }
+
+ next(rw, req)
+ })
+}
+
+func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ claims, err := authtypes.ClaimsFromContext(req.Context())
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ if err := claims.IsEditor(); err != nil {
+ middleware.logger.WarnContext(req.Context(), authzDeniedMessage, "claims", claims)
+ render.Error(rw, err)
+ return
+ }
+
+ next(rw, req)
+ })
+}
+
+func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ claims, err := authtypes.ClaimsFromContext(req.Context())
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ if err := claims.IsAdmin(); err != nil {
+ middleware.logger.WarnContext(req.Context(), authzDeniedMessage, "claims", claims)
+ render.Error(rw, err)
+ return
+ }
+
+ next(rw, req)
+ })
+}
+
+func (middleware *AuthZ) SelfAccess(next http.HandlerFunc) http.HandlerFunc {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ claims, err := authtypes.ClaimsFromContext(req.Context())
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ id := mux.Vars(req)["id"]
+ if err := claims.IsSelfAccess(id); err != nil {
+ middleware.logger.WarnContext(req.Context(), authzDeniedMessage, "claims", claims)
+ render.Error(rw, err)
+ return
+ }
+
+ next(rw, req)
+ })
+}
+
+func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ next(rw, req)
+ })
+}
+
+// Check middleware accepts the relation, typeable, parentTypeable (for direct access + group relations) and a callback function to derive selector and parentSelectors on per request basis.
+func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackFn) http.HandlerFunc {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ claims, err := authtypes.ClaimsFromContext(req.Context())
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ selector, err := cb(req.Context(), claims)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ err = middleware.authzService.CheckWithTupleCreation(req.Context(), claims, relation, typeable, selector)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ next(rw, req)
+ })
+}
diff --git a/ee/http/middleware/pat.go b/ee/http/middleware/pat.go
deleted file mode 100644
index 59a18a2b798..00000000000
--- a/ee/http/middleware/pat.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package middleware
-
-import (
- "net/http"
- "time"
-
- "github.com/SigNoz/signoz/pkg/sqlstore"
- "github.com/SigNoz/signoz/pkg/types"
- "github.com/SigNoz/signoz/pkg/types/authtypes"
- "go.uber.org/zap"
-)
-
-type Pat struct {
- store sqlstore.SQLStore
- uuid *authtypes.UUID
- headers []string
-}
-
-func NewPat(store sqlstore.SQLStore, headers []string) *Pat {
- return &Pat{store: store, uuid: authtypes.NewUUID(), headers: headers}
-}
-
-func (p *Pat) Wrap(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- var values []string
- var patToken string
- var pat types.StorableAPIKey
-
- for _, header := range p.headers {
- values = append(values, r.Header.Get(header))
- }
-
- ctx, err := p.uuid.ContextFromRequest(r.Context(), values...)
- if err != nil {
- next.ServeHTTP(w, r)
- return
- }
- patToken, ok := authtypes.UUIDFromContext(ctx)
- if !ok {
- next.ServeHTTP(w, r)
- return
- }
-
- err = p.store.BunDB().NewSelect().Model(&pat).Where("token = ?", patToken).Scan(r.Context())
- if err != nil {
- next.ServeHTTP(w, r)
- return
- }
-
- if pat.ExpiresAt.Before(time.Now()) {
- next.ServeHTTP(w, r)
- return
- }
-
- // get user from db
- user := types.User{}
- err = p.store.BunDB().NewSelect().Model(&user).Where("id = ?", pat.UserID).Scan(r.Context())
- if err != nil {
- next.ServeHTTP(w, r)
- return
- }
-
- jwt := authtypes.Claims{
- UserID: user.ID.String(),
- Role: pat.Role,
- Email: user.Email,
- OrgID: user.OrgID,
- }
-
- ctx = authtypes.NewContextWithClaims(ctx, jwt)
-
- r = r.WithContext(ctx)
-
- next.ServeHTTP(w, r)
-
- pat.LastUsed = time.Now()
- _, err = p.store.BunDB().NewUpdate().Model(&pat).Column("last_used").Where("token = ?", patToken).Where("revoked = false").Exec(r.Context())
- if err != nil {
- zap.L().Error("Failed to update PAT last used in db, err: %v", zap.Error(err))
- }
-
- })
-
-}
diff --git a/ee/licensing/config.go b/ee/licensing/config.go
new file mode 100644
index 00000000000..598724d8e36
--- /dev/null
+++ b/ee/licensing/config.go
@@ -0,0 +1,26 @@
+package licensing
+
+import (
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/SigNoz/signoz/pkg/licensing"
+)
+
+var (
+ config licensing.Config
+ once sync.Once
+)
+
+// initializes the licensing configuration
+func Config(pollInterval time.Duration, failureThreshold int) licensing.Config {
+ once.Do(func() {
+ config = licensing.Config{PollInterval: pollInterval, FailureThreshold: failureThreshold}
+ if err := config.Validate(); err != nil {
+ panic(fmt.Errorf("invalid licensing config: %w", err))
+ }
+ })
+
+ return config
+}
diff --git a/ee/licensing/httplicensing/api.go b/ee/licensing/httplicensing/api.go
new file mode 100644
index 00000000000..9f9bc1f5da9
--- /dev/null
+++ b/ee/licensing/httplicensing/api.go
@@ -0,0 +1,168 @@
+package httplicensing
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "github.com/SigNoz/signoz/pkg/errors"
+ "github.com/SigNoz/signoz/pkg/http/render"
+ "github.com/SigNoz/signoz/pkg/licensing"
+ "github.com/SigNoz/signoz/pkg/types/authtypes"
+ "github.com/SigNoz/signoz/pkg/types/licensetypes"
+ "github.com/SigNoz/signoz/pkg/valuer"
+)
+
+type licensingAPI struct {
+ licensing licensing.Licensing
+}
+
+func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
+ return &licensingAPI{licensing: licensing}
+}
+
+func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
+ defer cancel()
+
+ claims, err := authtypes.ClaimsFromContext(ctx)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ orgID, err := valuer.NewUUID(claims.OrgID)
+ if err != nil {
+ render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
+ return
+ }
+
+ req := new(licensetypes.PostableLicense)
+ err = json.NewDecoder(r.Body).Decode(&req)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ err = api.licensing.Activate(r.Context(), orgID, req.Key)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ render.Success(rw, http.StatusAccepted, nil)
+}
+
+func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
+ defer cancel()
+
+ claims, err := authtypes.ClaimsFromContext(ctx)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ orgID, err := valuer.NewUUID(claims.OrgID)
+ if err != nil {
+ render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
+ return
+ }
+
+ license, err := api.licensing.GetActive(r.Context(), orgID)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ gettableLicense := licensetypes.NewGettableLicense(license.Data, license.Key)
+ render.Success(rw, http.StatusOK, gettableLicense)
+}
+
+func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
+ defer cancel()
+
+ claims, err := authtypes.ClaimsFromContext(ctx)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ orgID, err := valuer.NewUUID(claims.OrgID)
+ if err != nil {
+ render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
+ return
+ }
+
+ err = api.licensing.Refresh(r.Context(), orgID)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ render.Success(rw, http.StatusNoContent, nil)
+}
+
+func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
+ defer cancel()
+
+ claims, err := authtypes.ClaimsFromContext(ctx)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ orgID, err := valuer.NewUUID(claims.OrgID)
+ if err != nil {
+ render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
+ return
+ }
+
+ req := new(licensetypes.PostableSubscription)
+ if err := json.NewDecoder(r.Body).Decode(req); err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ gettableSubscription, err := api.licensing.Checkout(ctx, orgID, req)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ render.Success(rw, http.StatusCreated, gettableSubscription)
+}
+
+func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
+ defer cancel()
+
+ claims, err := authtypes.ClaimsFromContext(ctx)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ orgID, err := valuer.NewUUID(claims.OrgID)
+ if err != nil {
+ render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
+ return
+ }
+
+ req := new(licensetypes.PostableSubscription)
+ if err := json.NewDecoder(r.Body).Decode(req); err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ gettableSubscription, err := api.licensing.Portal(ctx, orgID, req)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ render.Success(rw, http.StatusCreated, gettableSubscription)
+}
diff --git a/ee/licensing/httplicensing/provider.go b/ee/licensing/httplicensing/provider.go
new file mode 100644
index 00000000000..4bd2be39224
--- /dev/null
+++ b/ee/licensing/httplicensing/provider.go
@@ -0,0 +1,249 @@
+package httplicensing
+
+import (
+ "context"
+ "encoding/json"
+ "time"
+
+ "github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
+ "github.com/SigNoz/signoz/pkg/analytics"
+ "github.com/SigNoz/signoz/pkg/errors"
+ "github.com/SigNoz/signoz/pkg/factory"
+ "github.com/SigNoz/signoz/pkg/licensing"
+ "github.com/SigNoz/signoz/pkg/modules/organization"
+ "github.com/SigNoz/signoz/pkg/sqlstore"
+ "github.com/SigNoz/signoz/pkg/types/analyticstypes"
+ "github.com/SigNoz/signoz/pkg/types/licensetypes"
+ "github.com/SigNoz/signoz/pkg/valuer"
+ "github.com/SigNoz/signoz/pkg/zeus"
+ "github.com/tidwall/gjson"
+)
+
+type provider struct {
+ store licensetypes.Store
+ zeus zeus.Zeus
+ config licensing.Config
+ settings factory.ScopedProviderSettings
+ orgGetter organization.Getter
+ analytics analytics.Analytics
+ stopChan chan struct{}
+}
+
+func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
+ return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
+ return New(ctx, providerSettings, config, store, zeus, orgGetter, analytics)
+ })
+}
+
+func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) (licensing.Licensing, error) {
+ settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
+ licensestore := sqllicensingstore.New(sqlstore)
+ return &provider{
+ store: licensestore,
+ zeus: zeus,
+ config: config,
+ settings: settings,
+ orgGetter: orgGetter,
+ stopChan: make(chan struct{}),
+ analytics: analytics,
+ }, nil
+}
+
+func (provider *provider) Start(ctx context.Context) error {
+ tick := time.NewTicker(provider.config.PollInterval)
+ defer tick.Stop()
+
+ err := provider.Validate(ctx)
+ if err != nil {
+ provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err)
+ }
+
+ for {
+ select {
+ case <-provider.stopChan:
+ return nil
+ case <-tick.C:
+ err := provider.Validate(ctx)
+ if err != nil {
+ provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err)
+ }
+ }
+ }
+}
+
+func (provider *provider) Stop(ctx context.Context) error {
+ provider.settings.Logger().DebugContext(ctx, "license validation stopped")
+ close(provider.stopChan)
+ return nil
+}
+
+func (provider *provider) Validate(ctx context.Context) error {
+ organizations, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
+ if err != nil {
+ return err
+ }
+
+ for _, organization := range organizations {
+ err := provider.Refresh(ctx, organization.ID)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) error {
+ data, err := provider.zeus.GetLicense(ctx, key)
+ if err != nil {
+ return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
+ }
+
+ license, err := licensetypes.NewLicense(data, organizationID)
+ if err != nil {
+ return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
+ }
+
+ storableLicense := licensetypes.NewStorableLicenseFromLicense(license)
+ err = provider.store.Create(ctx, storableLicense)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (provider *provider) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) {
+ storableLicenses, err := provider.store.GetAll(ctx, organizationID)
+ if err != nil {
+ return nil, err
+ }
+
+ activeLicense, err := licensetypes.GetActiveLicenseFromStorableLicenses(storableLicenses, organizationID)
+ if err != nil {
+ return nil, err
+ }
+
+ return activeLicense, nil
+}
+
+func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error {
+ activeLicense, err := provider.GetActive(ctx, organizationID)
+ if err != nil {
+ if errors.Ast(err, errors.TypeNotFound) {
+ return nil
+ }
+ provider.settings.Logger().ErrorContext(ctx, "license validation failed", "org_id", organizationID.StringValue())
+ return err
+ }
+
+ data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
+ if err != nil {
+ if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
+ activeLicense.UpdateFeatures(licensetypes.BasicPlan)
+ updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
+ err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+ return err
+ }
+
+ err = activeLicense.Update(data)
+ if err != nil {
+ return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity from license data")
+ }
+
+ updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
+ err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
+ if err != nil {
+ return err
+ }
+
+ stats := licensetypes.NewStatsFromLicense(activeLicense)
+ provider.analytics.Send(ctx,
+ analyticstypes.Track{
+ UserId: "stats_" + organizationID.String(),
+ Event: "License Updated",
+ Properties: analyticstypes.NewPropertiesFromMap(stats),
+ Context: &analyticstypes.Context{
+ Extra: map[string]interface{}{
+ analyticstypes.KeyGroupID: organizationID.String(),
+ },
+ },
+ },
+ analyticstypes.Group{
+ UserId: "stats_" + organizationID.String(),
+ GroupId: organizationID.String(),
+ Traits: analyticstypes.NewTraitsFromMap(stats),
+ },
+ )
+
+ return nil
+}
+
+func (provider *provider) Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
+ activeLicense, err := provider.GetActive(ctx, organizationID)
+ if err != nil {
+ return nil, err
+ }
+
+ body, err := json.Marshal(postableSubscription)
+ if err != nil {
+ return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal checkout payload")
+ }
+
+ response, err := provider.zeus.GetCheckoutURL(ctx, activeLicense.Key, body)
+ if err != nil {
+ return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate checkout session")
+ }
+
+ return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
+}
+
+func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
+ activeLicense, err := provider.GetActive(ctx, organizationID)
+ if err != nil {
+ return nil, err
+ }
+
+ body, err := json.Marshal(postableSubscription)
+ if err != nil {
+ return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal portal payload")
+ }
+
+ response, err := provider.zeus.GetPortalURL(ctx, activeLicense.Key, body)
+ if err != nil {
+ return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate portal session")
+ }
+
+ return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
+}
+
+func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
+ license, err := provider.GetActive(ctx, organizationID)
+ if err != nil {
+ if errors.Ast(err, errors.TypeNotFound) {
+ return licensetypes.BasicPlan, nil
+ }
+ return nil, err
+ }
+
+ return license.Features, nil
+}
+
+func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
+ activeLicense, err := provider.GetActive(ctx, orgID)
+ if err != nil {
+ if errors.Ast(err, errors.TypeNotFound) {
+ return map[string]any{}, nil
+ }
+
+ return nil, err
+ }
+
+ return licensetypes.NewStatsFromLicense(activeLicense), nil
+}
diff --git a/ee/licensing/licensingstore/sqllicensingstore/store.go b/ee/licensing/licensingstore/sqllicensingstore/store.go
new file mode 100644
index 00000000000..dfbb257a934
--- /dev/null
+++ b/ee/licensing/licensingstore/sqllicensingstore/store.go
@@ -0,0 +1,81 @@
+package sqllicensingstore
+
+import (
+ "context"
+
+ "github.com/SigNoz/signoz/pkg/errors"
+ "github.com/SigNoz/signoz/pkg/sqlstore"
+ "github.com/SigNoz/signoz/pkg/types/licensetypes"
+ "github.com/SigNoz/signoz/pkg/valuer"
+)
+
+type store struct {
+ sqlstore sqlstore.SQLStore
+}
+
+func New(sqlstore sqlstore.SQLStore) licensetypes.Store {
+ return &store{sqlstore}
+}
+
+func (store *store) Create(ctx context.Context, storableLicense *licensetypes.StorableLicense) error {
+ _, err := store.
+ sqlstore.
+ BunDB().
+ NewInsert().
+ Model(storableLicense).
+ Exec(ctx)
+ if err != nil {
+ return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "license with ID: %s already exists", storableLicense.ID)
+ }
+
+ return nil
+}
+
+func (store *store) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.StorableLicense, error) {
+ storableLicense := new(licensetypes.StorableLicense)
+ err := store.
+ sqlstore.
+ BunDB().
+ NewSelect().
+ Model(storableLicense).
+ Where("org_id = ?", organizationID).
+ Where("id = ?", licenseID).
+ Scan(ctx)
+ if err != nil {
+ return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "license with ID: %s does not exist", licenseID)
+ }
+
+ return storableLicense, nil
+}
+
+func (store *store) GetAll(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.StorableLicense, error) {
+ storableLicenses := make([]*licensetypes.StorableLicense, 0)
+ err := store.
+ sqlstore.
+ BunDB().
+ NewSelect().
+ Model(&storableLicenses).
+ Where("org_id = ?", organizationID).
+ Scan(ctx)
+ if err != nil {
+ return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "licenses for organizationID: %s does not exists", organizationID)
+ }
+
+ return storableLicenses, nil
+}
+
+func (store *store) Update(ctx context.Context, organizationID valuer.UUID, storableLicense *licensetypes.StorableLicense) error {
+ _, err := store.
+ sqlstore.
+ BunDB().
+ NewUpdate().
+ Model(storableLicense).
+ WherePK().
+ Where("org_id = ?", organizationID).
+ Exec(ctx)
+ if err != nil {
+ return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update license with ID: %s", storableLicense.ID)
+ }
+
+ return nil
+}
diff --git a/ee/modules/user/impluser/handler.go b/ee/modules/user/impluser/handler.go
deleted file mode 100644
index 79ebe46b4cb..00000000000
--- a/ee/modules/user/impluser/handler.go
+++ /dev/null
@@ -1,405 +0,0 @@
-package impluser
-
-import (
- "context"
- "encoding/json"
- "net/http"
- "slices"
- "time"
-
- "github.com/SigNoz/signoz/pkg/errors"
- "github.com/SigNoz/signoz/pkg/http/render"
- "github.com/SigNoz/signoz/pkg/modules/user"
- "github.com/SigNoz/signoz/pkg/modules/user/impluser"
- "github.com/SigNoz/signoz/pkg/types"
- "github.com/SigNoz/signoz/pkg/types/authtypes"
- "github.com/SigNoz/signoz/pkg/valuer"
- "github.com/gorilla/mux"
-)
-
-// EnterpriseHandler embeds the base handler implementation
-type Handler struct {
- user.Handler // Embed the base handler interface
- module user.Module
-}
-
-func NewHandler(module user.Module) user.Handler {
- baseHandler := impluser.NewHandler(module)
- return &Handler{
- Handler: baseHandler,
- module: module,
- }
-}
-
-func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
-
- var req types.PostableLoginRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- render.Error(w, err)
- return
- }
-
- if req.RefreshToken == "" {
- // the EE handler wrapper passes the feature flag value in context
- ssoAvailable, ok := ctx.Value(types.SSOAvailable).(bool)
- if !ok {
- render.Error(w, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to retrieve SSO availability"))
- return
- }
-
- if ssoAvailable {
- _, err := h.module.CanUsePassword(ctx, req.Email)
- if err != nil {
- render.Error(w, err)
- return
- }
- }
- }
-
- user, err := h.module.GetAuthenticatedUser(ctx, req.OrgID, req.Email, req.Password, req.RefreshToken)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- jwt, err := h.module.GetJWTForUser(ctx, user)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- gettableLoginResponse := &types.GettableLoginResponse{
- GettableUserJwt: jwt,
- UserID: user.ID.String(),
- }
-
- render.Success(w, http.StatusOK, gettableLoginResponse)
-}
-
-// Override only the methods you need with enterprise-specific implementations
-func (h *Handler) LoginPrecheck(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
-
- // assume user is valid unless proven otherwise and assign default values for rest of the fields
-
- email := r.URL.Query().Get("email")
- sourceUrl := r.URL.Query().Get("ref")
- orgID := r.URL.Query().Get("orgID")
-
- resp, err := h.module.LoginPrecheck(ctx, orgID, email, sourceUrl)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- render.Success(w, http.StatusOK, resp)
-
-}
-
-func (h *Handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
-
- req := new(types.PostableAcceptInvite)
- if err := json.NewDecoder(r.Body).Decode(req); err != nil {
- render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode user"))
- return
- }
-
- // get invite object
- invite, err := h.module.GetInviteByToken(ctx, req.InviteToken)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- orgDomain, err := h.module.GetAuthDomainByEmail(ctx, invite.Email)
- if err != nil && !errors.Ast(err, errors.TypeNotFound) {
- render.Error(w, err)
- return
- }
-
- precheckResp := &types.GettableLoginPrecheck{
- SSO: false,
- IsUser: false,
- }
-
- if invite.Name == "" && req.DisplayName != "" {
- invite.Name = req.DisplayName
- }
-
- user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- if orgDomain != nil && orgDomain.SsoEnabled {
- // sso is enabled, create user and respond precheck data
- err = h.module.CreateUser(ctx, user)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- // check if sso is enforced for the org
- precheckResp, err = h.module.LoginPrecheck(ctx, invite.OrgID, user.Email, req.SourceURL)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- } else {
- password, err := types.NewFactorPassword(req.Password)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- user, err = h.module.CreateUserWithPassword(ctx, user, password)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- precheckResp.IsUser = true
- }
-
- // delete the invite
- if err := h.module.DeleteInvite(ctx, invite.OrgID, invite.ID); err != nil {
- render.Error(w, err)
- return
- }
-
- render.Success(w, http.StatusOK, precheckResp)
-}
-
-func (h *Handler) GetInvite(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
-
- token := mux.Vars(r)["token"]
- sourceUrl := r.URL.Query().Get("ref")
- invite, err := h.module.GetInviteByToken(ctx, token)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- // precheck the user
- precheckResp, err := h.module.LoginPrecheck(ctx, invite.OrgID, invite.Email, sourceUrl)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- gettableInvite := &types.GettableEEInvite{
- GettableInvite: *invite,
- PreCheck: precheckResp,
- }
-
- render.Success(w, http.StatusOK, gettableInvite)
- return
-}
-
-func (h *Handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
-
- claims, err := authtypes.ClaimsFromContext(ctx)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- userID, err := valuer.NewUUID(claims.UserID)
- if err != nil {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7"))
- return
- }
-
- req := new(types.PostableAPIKey)
- if err := json.NewDecoder(r.Body).Decode(req); err != nil {
- render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key"))
- return
- }
-
- apiKey, err := types.NewStorableAPIKey(
- req.Name,
- userID,
- req.Role,
- req.ExpiresInDays,
- )
- if err != nil {
- render.Error(w, err)
- return
- }
-
- err = h.module.CreateAPIKey(ctx, apiKey)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- // just corrected the status code, response is same,
- render.Success(w, http.StatusCreated, apiKey)
-}
-
-func (h *Handler) ListAPIKeys(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
-
- claims, err := authtypes.ClaimsFromContext(ctx)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- orgID, err := valuer.NewUUID(claims.OrgID)
- if err != nil {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
- return
- }
-
- apiKeys, err := h.module.ListAPIKeys(ctx, orgID)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- // for backward compatibility
- if len(apiKeys) == 0 {
- render.Success(w, http.StatusOK, []types.GettableAPIKey{})
- return
- }
-
- result := make([]*types.GettableAPIKey, len(apiKeys))
- for i, apiKey := range apiKeys {
- result[i] = types.NewGettableAPIKeyFromStorableAPIKey(apiKey)
- }
-
- render.Success(w, http.StatusOK, result)
-
-}
-
-func (h *Handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
-
- claims, err := authtypes.ClaimsFromContext(ctx)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- orgID, err := valuer.NewUUID(claims.OrgID)
- if err != nil {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
- return
- }
-
- userID, err := valuer.NewUUID(claims.UserID)
- if err != nil {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7"))
- return
- }
-
- req := types.StorableAPIKey{}
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key"))
- return
- }
-
- idStr := mux.Vars(r)["id"]
- id, err := valuer.NewUUID(idStr)
- if err != nil {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
- return
- }
-
- //get the API Key
- existingAPIKey, err := h.module.GetAPIKey(ctx, orgID, id)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- // get the user
- createdByUser, err := h.module.GetUserByID(ctx, orgID.String(), existingAPIKey.UserID.String())
- if err != nil {
- render.Error(w, err)
- return
- }
-
- if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
- return
- }
-
- err = h.module.UpdateAPIKey(ctx, id, &req, userID)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- render.Success(w, http.StatusNoContent, nil)
-}
-
-func (h *Handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
- defer cancel()
-
- claims, err := authtypes.ClaimsFromContext(ctx)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- idStr := mux.Vars(r)["id"]
- id, err := valuer.NewUUID(idStr)
- if err != nil {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
- return
- }
-
- orgID, err := valuer.NewUUID(claims.OrgID)
- if err != nil {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
- return
- }
-
- userID, err := valuer.NewUUID(claims.UserID)
- if err != nil {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7"))
- return
- }
-
- //get the API Key
- existingAPIKey, err := h.module.GetAPIKey(ctx, orgID, id)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- // get the user
- createdByUser, err := h.module.GetUserByID(ctx, orgID.String(), existingAPIKey.UserID.String())
- if err != nil {
- render.Error(w, err)
- return
- }
-
- if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
- render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
- return
- }
-
- if err := h.module.RevokeAPIKey(ctx, id, userID); err != nil {
- render.Error(w, err)
- return
- }
-
- render.Success(w, http.StatusNoContent, nil)
-}
diff --git a/ee/modules/user/impluser/module.go b/ee/modules/user/impluser/module.go
deleted file mode 100644
index 07d9c0a9e54..00000000000
--- a/ee/modules/user/impluser/module.go
+++ /dev/null
@@ -1,250 +0,0 @@
-package impluser
-
-import (
- "context"
- "fmt"
- "net/url"
- "strings"
-
- "github.com/SigNoz/signoz/ee/query-service/constants"
- "github.com/SigNoz/signoz/pkg/errors"
- "github.com/SigNoz/signoz/pkg/modules/user"
- baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
- "github.com/SigNoz/signoz/pkg/types"
- "github.com/SigNoz/signoz/pkg/types/authtypes"
- "github.com/SigNoz/signoz/pkg/valuer"
- "go.uber.org/zap"
-)
-
-// EnterpriseModule embeds the base module implementation
-type Module struct {
- user.Module // Embed the base module implementation
- store types.UserStore
-}
-
-func NewModule(store types.UserStore) user.Module {
- baseModule := baseimpl.NewModule(store)
- return &Module{
- Module: baseModule,
- store: store,
- }
-}
-
-func (m *Module) createUserForSAMLRequest(ctx context.Context, email string) (*types.User, error) {
- // get auth domain from email domain
- _, err := m.GetAuthDomainByEmail(ctx, email)
- if err != nil && !errors.Ast(err, errors.TypeNotFound) {
- return nil, err
- }
-
- // get name from email
- parts := strings.Split(email, "@")
- if len(parts) < 2 {
- return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid email format")
- }
- name := parts[0]
-
- defaultOrgID, err := m.store.GetDefaultOrgID(ctx)
- if err != nil {
- return nil, err
- }
-
- user, err := types.NewUser(name, email, types.RoleViewer.String(), defaultOrgID)
- if err != nil {
- return nil, err
- }
-
- err = m.CreateUser(ctx, user)
- if err != nil {
- return nil, err
- }
-
- return user, nil
-}
-
-func (m *Module) PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (string, error) {
- users, err := m.GetUsersByEmail(ctx, email)
- if err != nil {
- zap.L().Error("failed to get user with email received from auth provider", zap.String("error", err.Error()))
- return "", err
- }
- user := &types.User{}
-
- if len(users) == 0 {
- newUser, err := m.createUserForSAMLRequest(ctx, email)
- user = newUser
- if err != nil {
- zap.L().Error("failed to create user with email received from auth provider", zap.Error(err))
- return "", err
- }
- } else {
- user = &users[0].User
- }
-
- tokenStore, err := m.GetJWTForUser(ctx, user)
- if err != nil {
- zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
- return "", err
- }
-
- return fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
- redirectUri,
- tokenStore.AccessJwt,
- user.ID,
- tokenStore.RefreshJwt), nil
-}
-
-func (m *Module) CanUsePassword(ctx context.Context, email string) (bool, error) {
- domain, err := m.GetAuthDomainByEmail(ctx, email)
- if err != nil && !errors.Ast(err, errors.TypeNotFound) {
- return false, err
- }
-
- if domain != nil && domain.SsoEnabled {
- // sso is enabled, check if the user has admin role
- users, err := m.GetUsersByEmail(ctx, email)
- if err != nil {
- return false, err
- }
-
- if len(users) == 0 {
- return false, errors.New(errors.TypeNotFound, errors.CodeNotFound, "user not found")
- }
-
- if users[0].Role != types.RoleAdmin.String() {
- return false, errors.New(errors.TypeForbidden, errors.CodeForbidden, "auth method not supported")
- }
-
- }
-
- return true, nil
-}
-
-func (m *Module) LoginPrecheck(ctx context.Context, orgID, email, sourceUrl string) (*types.GettableLoginPrecheck, error) {
- resp := &types.GettableLoginPrecheck{IsUser: true, CanSelfRegister: false}
-
- // check if email is a valid user
- users, err := m.GetUsersByEmail(ctx, email)
- if err != nil {
- return nil, err
- }
-
- if len(users) == 0 {
- resp.IsUser = false
- }
-
- // give them an option to select an org
- if orgID == "" && len(users) > 1 {
- resp.SelectOrg = true
- resp.Orgs = make([]string, len(users))
- for i, user := range users {
- resp.Orgs[i] = user.OrgID
- }
- return resp, nil
- }
-
- // select the user with the corresponding orgID
- if len(users) > 1 {
- found := false
- for _, tuser := range users {
- if tuser.OrgID == orgID {
- // user = tuser
- found = true
- break
- }
- }
- if !found {
- resp.IsUser = false
- return resp, nil
- }
- }
-
- // the EE handler wrapper passes the feature flag value in context
- ssoAvailable, ok := ctx.Value(types.SSOAvailable).(bool)
- if !ok {
- zap.L().Error("failed to retrieve ssoAvailable from context")
- return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to retrieve SSO availability")
- }
-
- if ssoAvailable {
-
- // TODO(Nitya): in multitenancy this should use orgId as well.
- orgDomain, err := m.GetAuthDomainByEmail(ctx, email)
- if err != nil && !errors.Ast(err, errors.TypeNotFound) {
- return nil, err
- }
-
- if orgDomain != nil && orgDomain.SsoEnabled {
- // this is to allow self registration
- resp.IsUser = true
-
- // saml is enabled for this domain, lets prepare sso url
- if sourceUrl == "" {
- sourceUrl = constants.GetDefaultSiteURL()
- }
-
- // parse source url that generated the login request
- var err error
- escapedUrl, _ := url.QueryUnescape(sourceUrl)
- siteUrl, err := url.Parse(escapedUrl)
- if err != nil {
- return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse referer")
- }
-
- // build Idp URL that will authenticat the user
- // the front-end will redirect user to this url
- resp.SSOUrl, err = orgDomain.BuildSsoUrl(siteUrl)
- if err != nil {
- zap.L().Error("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), zap.Error(err))
- return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to prepare saml request for domain")
- }
-
- // set SSO to true, as the url is generated correctly
- resp.SSO = true
- }
- }
- return resp, nil
-}
-
-func (m *Module) GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error) {
-
- if email == "" {
- return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
- }
-
- components := strings.Split(email, "@")
- if len(components) < 2 {
- return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid email format")
- }
-
- domain, err := m.store.GetDomainByName(ctx, components[1])
- if err != nil {
- return nil, err
- }
-
- gettableDomain := &types.GettableOrgDomain{StorableOrgDomain: *domain}
- if err := gettableDomain.LoadConfig(domain.Data); err != nil {
- return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to load domain config")
- }
- return gettableDomain, nil
-}
-
-func (m *Module) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error {
- return m.store.CreateAPIKey(ctx, apiKey)
-}
-
-func (m *Module) UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error {
- return m.store.UpdateAPIKey(ctx, id, apiKey, updaterID)
-}
-
-func (m *Module) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) {
- return m.store.ListAPIKeys(ctx, orgID)
-}
-
-func (m *Module) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*types.StorableAPIKeyUser, error) {
- return m.store.GetAPIKey(ctx, orgID, id)
-}
-
-func (m *Module) RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error {
- return m.store.RevokeAPIKey(ctx, id, removedByUserID)
-}
diff --git a/ee/modules/user/impluser/store.go b/ee/modules/user/impluser/store.go
deleted file mode 100644
index cbd23478d79..00000000000
--- a/ee/modules/user/impluser/store.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package impluser
-
-import (
- "context"
-
- "github.com/SigNoz/signoz/pkg/errors"
- baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
- "github.com/SigNoz/signoz/pkg/sqlstore"
- "github.com/SigNoz/signoz/pkg/types"
-)
-
-type store struct {
- *baseimpl.Store
- sqlstore sqlstore.SQLStore
-}
-
-func NewStore(sqlstore sqlstore.SQLStore) types.UserStore {
- baseStore := baseimpl.NewStore(sqlstore).(*baseimpl.Store)
- return &store{
- Store: baseStore,
- sqlstore: sqlstore,
- }
-}
-
-func (s *store) GetDomainByName(ctx context.Context, name string) (*types.StorableOrgDomain, error) {
- domain := new(types.StorableOrgDomain)
- err := s.sqlstore.BunDB().NewSelect().
- Model(domain).
- Where("name = ?", name).
- Limit(1).
- Scan(ctx)
-
- if err != nil {
- return nil, errors.Wrapf(err, errors.TypeNotFound, errors.CodeNotFound, "failed to get domain from name")
- }
- return domain, nil
-}
diff --git a/ee/query-service/.dockerignore b/ee/query-service/.dockerignore
deleted file mode 100644
index 9521c5060b3..00000000000
--- a/ee/query-service/.dockerignore
+++ /dev/null
@@ -1,4 +0,0 @@
-.vscode
-README.md
-signoz.db
-bin
\ No newline at end of file
diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go
index 812b83b73d4..c5fcbf64f47 100644
--- a/ee/query-service/app/api/api.go
+++ b/ee/query-service/app/api/api.go
@@ -1,47 +1,34 @@
package api
import (
- "context"
"net/http"
"net/http/httputil"
"time"
- "github.com/SigNoz/signoz/ee/query-service/dao"
+ "github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
- "github.com/SigNoz/signoz/ee/query-service/interfaces"
- "github.com/SigNoz/signoz/ee/query-service/license"
- "github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
- "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/middleware"
- "github.com/SigNoz/signoz/pkg/http/render"
- "github.com/SigNoz/signoz/pkg/modules/quickfilter"
- quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
+ querierAPI "github.com/SigNoz/signoz/pkg/querier"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
- baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
+ "github.com/SigNoz/signoz/pkg/query-service/interfaces"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/signoz"
- "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/gorilla/mux"
- "go.uber.org/zap"
)
type APIHandlerOptions struct {
- DataConnector interfaces.DataConnector
- PreferSpanMetrics bool
- AppDao dao.ModelDao
+ DataConnector interfaces.Reader
RulesManager *rules.Manager
UsageManager *usage.Manager
- FeatureFlags baseint.FeatureLookup
- LicenseManager *license.Manager
IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
@@ -61,22 +48,18 @@ type APIHandler struct {
// NewAPIHandler returns an APIHandler
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
- quickfiltermodule := quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(signoz.SQLStore))
- quickFilter := quickfilter.NewAPI(quickfiltermodule)
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
Reader: opts.DataConnector,
- PreferSpanMetrics: opts.PreferSpanMetrics,
RuleManager: opts.RulesManager,
- FeatureFlags: opts.FeatureFlags,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
- FieldsAPI: fields.NewAPI(signoz.TelemetryStore),
+ LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
+ FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
Signoz: signoz,
- QuickFilters: quickFilter,
- QuickFilterModule: quickfiltermodule,
+ QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
})
if err != nil {
@@ -90,83 +73,48 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
return ah, nil
}
-func (ah *APIHandler) FF() baseint.FeatureLookup {
- return ah.opts.FeatureFlags
-}
-
func (ah *APIHandler) RM() *rules.Manager {
return ah.opts.RulesManager
}
-func (ah *APIHandler) LM() *license.Manager {
- return ah.opts.LicenseManager
-}
-
func (ah *APIHandler) UM() *usage.Manager {
return ah.opts.UsageManager
}
-func (ah *APIHandler) AppDao() dao.ModelDao {
- return ah.opts.AppDao
-}
-
func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
return ah.opts.Gateway
}
-func (ah *APIHandler) CheckFeature(f string) bool {
- err := ah.FF().CheckFeature(f)
- return err == nil
-}
-
// RegisterRoutes registers routes for this handler on the given router
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first
// routes available only in ee version
-
- router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
- router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.loginPrecheck)).Methods(http.MethodGet)
-
- // invite
- router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.getInvite)).Methods(http.MethodGet)
- router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.acceptInvite)).Methods(http.MethodPost)
+ router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
// paid plans specific routes
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
- router.HandleFunc("/api/v1/complete/google", am.OpenAccess(ah.receiveGoogleAuth)).Methods(http.MethodGet)
- router.HandleFunc("/api/v1/orgs/{orgId}/domains", am.AdminAccess(ah.listDomainsByOrg)).Methods(http.MethodGet)
-
- router.HandleFunc("/api/v1/domains", am.AdminAccess(ah.postDomain)).Methods(http.MethodPost)
- router.HandleFunc("/api/v1/domains/{id}", am.AdminAccess(ah.putDomain)).Methods(http.MethodPut)
- router.HandleFunc("/api/v1/domains/{id}", am.AdminAccess(ah.deleteDomain)).Methods(http.MethodDelete)
// base overrides
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
- router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
-
- // PAT APIs
- router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.Signoz.Handlers.User.CreateAPIKey)).Methods(http.MethodPost)
- router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.Signoz.Handlers.User.ListAPIKeys)).Methods(http.MethodGet)
- router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.UpdateAPIKey)).Methods(http.MethodPut)
- router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.RevokeAPIKey)).Methods(http.MethodDelete)
- router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost)
+ router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingAPI.Checkout)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
- router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.portalSession)).Methods(http.MethodPost)
-
- router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
- router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
+ router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
// v3
- router.HandleFunc("/api/v3/licenses", am.ViewAccess(ah.listLicensesV3)).Methods(http.MethodGet)
- router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.applyLicenseV3)).Methods(http.MethodPost)
- router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.refreshLicensesV3)).Methods(http.MethodPut)
- router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.getActiveLicenseV3)).Methods(http.MethodGet)
+ router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
+ router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
+ router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.LicensingAPI.GetActive)).Methods(http.MethodGet)
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
+ // v5
+ router.HandleFunc("/api/v5/query_range", am.ViewAccess(ah.queryRangeV5)).Methods(http.MethodPost)
+
+ router.HandleFunc("/api/v5/substitute_vars", am.ViewAccess(ah.QuerierAPI.ReplaceVariables)).Methods(http.MethodPost)
+
// Gateway
router.PathPrefix(gateway.RoutePrefix).HandlerFunc(am.EditAccess(ah.ServeGatewayHTTP))
@@ -174,54 +122,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
}
-// TODO(nitya): remove this once we know how to get the FF's
-func (ah *APIHandler) updateRequestContext(w http.ResponseWriter, r *http.Request) (*http.Request, error) {
- ssoAvailable := true
- err := ah.FF().CheckFeature(model.SSO)
- if err != nil {
- switch err.(type) {
- case basemodel.ErrFeatureUnavailable:
- // do nothing, just skip sso
- ssoAvailable = false
- default:
- zap.L().Error("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err))
- return r, errors.New(errors.TypeInternal, errors.CodeInternal, "error checking SSO feature")
- }
- }
- ctx := context.WithValue(r.Context(), types.SSOAvailable, ssoAvailable)
- return r.WithContext(ctx), nil
-}
-
-func (ah *APIHandler) loginPrecheck(w http.ResponseWriter, r *http.Request) {
- r, err := ah.updateRequestContext(w, r)
- if err != nil {
- render.Error(w, err)
- return
- }
- ah.Signoz.Handlers.User.LoginPrecheck(w, r)
- return
-}
-
-func (ah *APIHandler) acceptInvite(w http.ResponseWriter, r *http.Request) {
- r, err := ah.updateRequestContext(w, r)
- if err != nil {
- render.Error(w, err)
- return
- }
- ah.Signoz.Handlers.User.AcceptInvite(w, r)
- return
-}
-
-func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
- r, err := ah.updateRequestContext(w, r)
- if err != nil {
- render.Error(w, err)
- return
- }
- ah.Signoz.Handlers.User.GetInvite(w, r)
- return
-}
-
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
diff --git a/ee/query-service/app/api/auth.go b/ee/query-service/app/api/auth.go
index 1d90df80d5b..3ffa247440c 100644
--- a/ee/query-service/app/api/auth.go
+++ b/ee/query-service/app/api/auth.go
@@ -3,41 +3,16 @@ package api
import (
"context"
"encoding/base64"
- "encoding/json"
"fmt"
- "io"
"net/http"
"net/url"
"go.uber.org/zap"
- "github.com/SigNoz/signoz/ee/query-service/constants"
- "github.com/SigNoz/signoz/ee/query-service/model"
- "github.com/SigNoz/signoz/pkg/http/render"
+ "github.com/SigNoz/signoz/pkg/query-service/constants"
+ "github.com/SigNoz/signoz/pkg/valuer"
)
-func parseRequest(r *http.Request, req interface{}) error {
- defer r.Body.Close()
- requestBody, err := io.ReadAll(r.Body)
- if err != nil {
- return err
- }
-
- err = json.Unmarshal(requestBody, &req)
- return err
-}
-
-// loginUser overrides base handler and considers SSO case.
-func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
- r, err := ah.updateRequestContext(w, r)
- if err != nil {
- render.Error(w, err)
- return
- }
- ah.Signoz.Handlers.User.Login(w, r)
- return
-}
-
func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) {
ssoError := []byte("Login failed. Please contact your system administrator")
dst := make([]byte, base64.StdEncoding.EncodedLen(len(ssoError)))
@@ -46,84 +21,12 @@ func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string)
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectURL, string(dst)), http.StatusSeeOther)
}
-// receiveGoogleAuth completes google OAuth response and forwards a request
-// to front-end to sign user in
-func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) {
- redirectUri := constants.GetDefaultSiteURL()
- ctx := context.Background()
-
- if !ah.CheckFeature(model.SSO) {
- zap.L().Error("[receiveGoogleAuth] sso requested but feature unavailable in org domain")
- http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
- return
- }
-
- q := r.URL.Query()
- if errType := q.Get("error"); errType != "" {
- zap.L().Error("[receiveGoogleAuth] failed to login with google auth", zap.String("error", errType), zap.String("error_description", q.Get("error_description")))
- http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "failed to login through SSO "), http.StatusMovedPermanently)
- return
- }
-
- relayState := q.Get("state")
- zap.L().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState))
-
- parsedState, err := url.Parse(relayState)
- if err != nil || relayState == "" {
- zap.L().Error("[receiveGoogleAuth] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
- handleSsoError(w, r, redirectUri)
- return
- }
-
- // upgrade redirect url from the relay state for better accuracy
- redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
-
- // fetch domain by parsing relay state.
- domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
- if err != nil {
- handleSsoError(w, r, redirectUri)
- return
- }
-
- // now that we have domain, use domain to fetch sso settings.
- // prepare google callback handler using parsedState -
- // which contains redirect URL (front-end endpoint)
- callbackHandler, err := domain.PrepareGoogleOAuthProvider(parsedState)
- if err != nil {
- zap.L().Error("[receiveGoogleAuth] failed to prepare google oauth provider", zap.String("domain", domain.String()), zap.Error(err))
- handleSsoError(w, r, redirectUri)
- return
- }
-
- identity, err := callbackHandler.HandleCallback(r)
- if err != nil {
- zap.L().Error("[receiveGoogleAuth] failed to process HandleCallback ", zap.String("domain", domain.String()), zap.Error(err))
- handleSsoError(w, r, redirectUri)
- return
- }
-
- nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, identity.Email, ah.opts.JWT)
- if err != nil {
- zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
- handleSsoError(w, r, redirectUri)
- return
- }
-
- http.Redirect(w, r, nextPage, http.StatusSeeOther)
-}
-
// receiveSAML completes a SAML request and gets user logged in
func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
// this is the source url that initiated the login request
redirectUri := constants.GetDefaultSiteURL()
ctx := context.Background()
- if !ah.CheckFeature(model.SSO) {
- zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
- http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
- return
- }
-
err := r.ParseForm()
if err != nil {
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
@@ -147,12 +50,25 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
// fetch domain by parsing relay state.
- domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
+ domain, err := ah.Signoz.Modules.User.GetDomainFromSsoResponse(ctx, parsedState)
if err != nil {
handleSsoError(w, r, redirectUri)
return
}
+ orgID, err := valuer.NewUUID(domain.OrgID)
+ if err != nil {
+ handleSsoError(w, r, redirectUri)
+ return
+ }
+
+ _, err = ah.Signoz.Licensing.GetActive(ctx, orgID)
+ if err != nil {
+ zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
+ http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
+ return
+ }
+
sp, err := domain.PrepareSamlRequest(parsedState)
if err != nil {
zap.L().Error("[receiveSAML] failed to prepare saml request for domain", zap.String("domain", domain.String()), zap.Error(err))
@@ -180,7 +96,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
return
}
- nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email, ah.opts.JWT)
+ nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email)
if err != nil {
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
diff --git a/ee/query-service/app/api/cloudIntegrations.go b/ee/query-service/app/api/cloudIntegrations.go
index 1251a524032..101646e4ee2 100644
--- a/ee/query-service/app/api/cloudIntegrations.go
+++ b/ee/query-service/app/api/cloudIntegrations.go
@@ -13,11 +13,11 @@ import (
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
+ "github.com/SigNoz/signoz/pkg/modules/user"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
- "github.com/google/uuid"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
@@ -36,6 +36,12 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
+ orgID, err := valuer.NewUUID(claims.OrgID)
+ if err != nil {
+ render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
+ return
+ }
+
cloudProvider := mux.Vars(r)["cloudProvider"]
if cloudProvider != "aws" {
RespondError(w, basemodel.BadRequest(fmt.Errorf(
@@ -56,11 +62,9 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
SigNozAPIKey: apiKey,
}
- license, apiErr := ah.LM().GetRepo().GetActiveLicense(r.Context())
- if apiErr != nil {
- RespondError(w, basemodel.WrapApiError(
- apiErr, "couldn't look for active license",
- ), nil)
+ license, err := ah.Signoz.Licensing.GetActive(r.Context(), orgID)
+ if err != nil {
+ render.Error(w, err)
return
}
@@ -188,14 +192,14 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
))
}
- password, err := types.NewFactorPassword(uuid.NewString())
+ password := types.MustGenerateFactorPassword(newUser.ID.StringValue())
- integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password)
+ err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}
- return integrationUser, nil
+ return newUser, nil
}
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
diff --git a/ee/query-service/app/api/dashboard.go b/ee/query-service/app/api/dashboard.go
deleted file mode 100644
index a0e30ecf8c7..00000000000
--- a/ee/query-service/app/api/dashboard.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package api
-
-import (
- "net/http"
- "strings"
-
- "github.com/SigNoz/signoz/pkg/errors"
- "github.com/SigNoz/signoz/pkg/http/render"
- "github.com/SigNoz/signoz/pkg/types/authtypes"
- "github.com/gorilla/mux"
-)
-
-func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) {
- ah.lockUnlockDashboard(w, r, true)
-}
-
-func (ah *APIHandler) unlockDashboard(w http.ResponseWriter, r *http.Request) {
- ah.lockUnlockDashboard(w, r, false)
-}
-
-func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request, lock bool) {
- // Locking can only be done by the owner of the dashboard
- // or an admin
-
- // - Fetch the dashboard
- // - Check if the user is the owner or an admin
- // - If yes, lock/unlock the dashboard
- // - If no, return 403
-
- // Get the dashboard UUID from the request
- uuid := mux.Vars(r)["uuid"]
- if strings.HasPrefix(uuid, "integration") {
- render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "dashboards created by integrations cannot be modified"))
- return
- }
-
- claims, err := authtypes.ClaimsFromContext(r.Context())
- if err != nil {
- render.Error(w, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
- return
- }
-
- dashboard, err := ah.Signoz.Modules.Dashboard.Get(r.Context(), claims.OrgID, uuid)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- if err := claims.IsAdmin(); err != nil && (dashboard.CreatedBy != claims.Email) {
- render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "You are not authorized to lock/unlock this dashboard"))
- return
- }
-
- // Lock/Unlock the dashboard
- err = ah.Signoz.Modules.Dashboard.LockUnlock(r.Context(), claims.OrgID, uuid, lock)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- ah.Respond(w, "Dashboard updated successfully")
-}
diff --git a/ee/query-service/app/api/domains.go b/ee/query-service/app/api/domains.go
deleted file mode 100644
index 770c2048f98..00000000000
--- a/ee/query-service/app/api/domains.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package api
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
-
- "github.com/SigNoz/signoz/ee/query-service/model"
- "github.com/SigNoz/signoz/pkg/types"
- "github.com/google/uuid"
- "github.com/gorilla/mux"
-)
-
-func (ah *APIHandler) listDomainsByOrg(w http.ResponseWriter, r *http.Request) {
- orgId := mux.Vars(r)["orgId"]
- domains, apierr := ah.AppDao().ListDomains(context.Background(), orgId)
- if apierr != nil {
- RespondError(w, apierr, domains)
- return
- }
- ah.Respond(w, domains)
-}
-
-func (ah *APIHandler) postDomain(w http.ResponseWriter, r *http.Request) {
- ctx := context.Background()
-
- req := types.GettableOrgDomain{}
-
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- RespondError(w, model.BadRequest(err), nil)
- return
- }
-
- if err := req.ValidNew(); err != nil {
- RespondError(w, model.BadRequest(err), nil)
- return
- }
-
- if apierr := ah.AppDao().CreateDomain(ctx, &req); apierr != nil {
- RespondError(w, apierr, nil)
- return
- }
-
- ah.Respond(w, &req)
-}
-
-func (ah *APIHandler) putDomain(w http.ResponseWriter, r *http.Request) {
- ctx := context.Background()
-
- domainIdStr := mux.Vars(r)["id"]
- domainId, err := uuid.Parse(domainIdStr)
- if err != nil {
- RespondError(w, model.BadRequest(err), nil)
- return
- }
-
- req := types.GettableOrgDomain{StorableOrgDomain: types.StorableOrgDomain{ID: domainId}}
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- RespondError(w, model.BadRequest(err), nil)
- return
- }
- req.ID = domainId
- if err := req.Valid(nil); err != nil {
- RespondError(w, model.BadRequest(err), nil)
- }
-
- if apierr := ah.AppDao().UpdateDomain(ctx, &req); apierr != nil {
- RespondError(w, apierr, nil)
- return
- }
-
- ah.Respond(w, &req)
-}
-
-func (ah *APIHandler) deleteDomain(w http.ResponseWriter, r *http.Request) {
- domainIdStr := mux.Vars(r)["id"]
-
- domainId, err := uuid.Parse(domainIdStr)
- if err != nil {
- RespondError(w, model.BadRequest(fmt.Errorf("invalid domain id")), nil)
- return
- }
-
- apierr := ah.AppDao().DeleteDomain(context.Background(), domainId)
- if apierr != nil {
- RespondError(w, apierr, nil)
- return
- }
- ah.Respond(w, nil)
-}
diff --git a/ee/query-service/app/api/featureFlags.go b/ee/query-service/app/api/featureFlags.go
index 1feca4b7645..a01ad40e87c 100644
--- a/ee/query-service/app/api/featureFlags.go
+++ b/ee/query-service/app/api/featureFlags.go
@@ -9,13 +9,29 @@ import (
"time"
"github.com/SigNoz/signoz/ee/query-service/constants"
- basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
+ pkgError "github.com/SigNoz/signoz/pkg/errors"
+ "github.com/SigNoz/signoz/pkg/http/render"
+ "github.com/SigNoz/signoz/pkg/types/authtypes"
+ "github.com/SigNoz/signoz/pkg/types/licensetypes"
+ "github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- featureSet, err := ah.FF().GetFeatureFlags()
+ claims, err := authtypes.ClaimsFromContext(ctx)
+ if err != nil {
+ render.Error(w, err)
+ return
+ }
+
+ orgID, err := valuer.NewUUID(claims.OrgID)
+ if err != nil {
+ render.Error(w, pkgError.Newf(pkgError.TypeInvalidInput, pkgError.CodeInvalidInput, "orgId is invalid"))
+ return
+ }
+
+ featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context(), orgID)
if err != nil {
ah.HandleError(w, err, http.StatusInternalServerError)
return
@@ -23,7 +39,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
if constants.FetchFeatures == "true" {
zap.L().Debug("fetching license")
- license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
+ license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
if err != nil {
zap.L().Error("failed to fetch license", zap.Error(err))
} else if license == nil {
@@ -43,10 +59,17 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
}
}
- if ah.opts.PreferSpanMetrics {
- for idx := range featureSet {
- feature := &featureSet[idx]
- if feature.Name == basemodel.UseSpanMetrics {
+ if constants.IsPreferSpanMetrics {
+ for idx, feature := range featureSet {
+ if feature.Name == licensetypes.UseSpanMetrics {
+ featureSet[idx].Active = true
+ }
+ }
+ }
+
+ if constants.IsDotMetricsEnabled {
+ for idx, feature := range featureSet {
+ if feature.Name == licensetypes.DotMetricsEnabled {
featureSet[idx].Active = true
}
}
@@ -57,7 +80,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
// and returns the FeatureSet.
-func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
+func fetchZeusFeatures(url, licenseKey string) ([]*licensetypes.Feature, error) {
// Check if the URL is empty
if url == "" {
return nil, fmt.Errorf("url is empty")
@@ -116,28 +139,28 @@ func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
}
type ZeusFeaturesResponse struct {
- Status string `json:"status"`
- Data basemodel.FeatureSet `json:"data"`
+ Status string `json:"status"`
+ Data []*licensetypes.Feature `json:"data"`
}
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
-func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basemodel.FeatureSet {
+func MergeFeatureSets(zeusFeatures, internalFeatures []*licensetypes.Feature) []*licensetypes.Feature {
// Create a map to store the merged features
- featureMap := make(map[string]basemodel.Feature)
+ featureMap := make(map[string]*licensetypes.Feature)
// Add all features from the otherFeatures set to the map
for _, feature := range internalFeatures {
- featureMap[feature.Name] = feature
+ featureMap[feature.Name.StringValue()] = feature
}
// Add all features from the zeusFeatures set to the map
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
for _, feature := range zeusFeatures {
- featureMap[feature.Name] = feature
+ featureMap[feature.Name.StringValue()] = feature
}
// Convert the map back to a FeatureSet slice
- var mergedFeatures basemodel.FeatureSet
+ var mergedFeatures []*licensetypes.Feature
for _, feature := range featureMap {
mergedFeatures = append(mergedFeatures, feature)
}
diff --git a/ee/query-service/app/api/featureFlags_test.go b/ee/query-service/app/api/featureFlags_test.go
index e64e2ea135c..96194e41fa3 100644
--- a/ee/query-service/app/api/featureFlags_test.go
+++ b/ee/query-service/app/api/featureFlags_test.go
@@ -3,78 +3,79 @@ package api
import (
"testing"
- basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
+ "github.com/SigNoz/signoz/pkg/types/licensetypes"
+ "github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
)
func TestMergeFeatureSets(t *testing.T) {
tests := []struct {
name string
- zeusFeatures basemodel.FeatureSet
- internalFeatures basemodel.FeatureSet
- expected basemodel.FeatureSet
+ zeusFeatures []*licensetypes.Feature
+ internalFeatures []*licensetypes.Feature
+ expected []*licensetypes.Feature
}{
{
name: "empty zeusFeatures and internalFeatures",
- zeusFeatures: basemodel.FeatureSet{},
- internalFeatures: basemodel.FeatureSet{},
- expected: basemodel.FeatureSet{},
+ zeusFeatures: []*licensetypes.Feature{},
+ internalFeatures: []*licensetypes.Feature{},
+ expected: []*licensetypes.Feature{},
},
{
name: "non-empty zeusFeatures and empty internalFeatures",
- zeusFeatures: basemodel.FeatureSet{
- {Name: "Feature1", Active: true},
- {Name: "Feature2", Active: false},
+ zeusFeatures: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature1"), Active: true},
+ {Name: valuer.NewString("Feature2"), Active: false},
},
- internalFeatures: basemodel.FeatureSet{},
- expected: basemodel.FeatureSet{
- {Name: "Feature1", Active: true},
- {Name: "Feature2", Active: false},
+ internalFeatures: []*licensetypes.Feature{},
+ expected: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature1"), Active: true},
+ {Name: valuer.NewString("Feature2"), Active: false},
},
},
{
name: "empty zeusFeatures and non-empty internalFeatures",
- zeusFeatures: basemodel.FeatureSet{},
- internalFeatures: basemodel.FeatureSet{
- {Name: "Feature1", Active: true},
- {Name: "Feature2", Active: false},
+ zeusFeatures: []*licensetypes.Feature{},
+ internalFeatures: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature1"), Active: true},
+ {Name: valuer.NewString("Feature2"), Active: false},
},
- expected: basemodel.FeatureSet{
- {Name: "Feature1", Active: true},
- {Name: "Feature2", Active: false},
+ expected: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature1"), Active: true},
+ {Name: valuer.NewString("Feature2"), Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
- zeusFeatures: basemodel.FeatureSet{
- {Name: "Feature1", Active: true},
- {Name: "Feature3", Active: false},
+ zeusFeatures: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature1"), Active: true},
+ {Name: valuer.NewString("Feature3"), Active: false},
},
- internalFeatures: basemodel.FeatureSet{
- {Name: "Feature2", Active: true},
- {Name: "Feature4", Active: false},
+ internalFeatures: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature2"), Active: true},
+ {Name: valuer.NewString("Feature4"), Active: false},
},
- expected: basemodel.FeatureSet{
- {Name: "Feature1", Active: true},
- {Name: "Feature2", Active: true},
- {Name: "Feature3", Active: false},
- {Name: "Feature4", Active: false},
+ expected: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature1"), Active: true},
+ {Name: valuer.NewString("Feature2"), Active: true},
+ {Name: valuer.NewString("Feature3"), Active: false},
+ {Name: valuer.NewString("Feature4"), Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
- zeusFeatures: basemodel.FeatureSet{
- {Name: "Feature1", Active: true},
- {Name: "Feature2", Active: false},
+ zeusFeatures: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature1"), Active: true},
+ {Name: valuer.NewString("Feature2"), Active: false},
},
- internalFeatures: basemodel.FeatureSet{
- {Name: "Feature1", Active: false},
- {Name: "Feature3", Active: true},
+ internalFeatures: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature1"), Active: false},
+ {Name: valuer.NewString("Feature3"), Active: true},
},
- expected: basemodel.FeatureSet{
- {Name: "Feature1", Active: true},
- {Name: "Feature2", Active: false},
- {Name: "Feature3", Active: true},
+ expected: []*licensetypes.Feature{
+ {Name: valuer.NewString("Feature1"), Active: true},
+ {Name: valuer.NewString("Feature2"), Active: false},
+ {Name: valuer.NewString("Feature3"), Active: true},
},
},
}
diff --git a/ee/query-service/app/api/gateway.go b/ee/query-service/app/api/gateway.go
index 54fc1759ed1..fa1d52153fe 100644
--- a/ee/query-service/app/api/gateway.go
+++ b/ee/query-service/app/api/gateway.go
@@ -5,10 +5,26 @@ import (
"strings"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
+ "github.com/SigNoz/signoz/pkg/errors"
+ "github.com/SigNoz/signoz/pkg/http/render"
+ "github.com/SigNoz/signoz/pkg/types/authtypes"
+ "github.com/SigNoz/signoz/pkg/valuer"
)
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
+ claims, err := authtypes.ClaimsFromContext(ctx)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ orgID, err := valuer.NewUUID(claims.OrgID)
+ if err != nil {
+ render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
+ return
+ }
+
validPath := false
for _, allowedPrefix := range gateway.AllowedPrefix {
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
@@ -22,9 +38,9 @@ func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request
return
}
- license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
+ license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
if err != nil {
- RespondError(rw, err, nil)
+ render.Error(rw, err)
return
}
diff --git a/ee/query-service/app/api/license.go b/ee/query-service/app/api/license.go
index 8fbbb0cccc6..73b4ae56ace 100644
--- a/ee/query-service/app/api/license.go
+++ b/ee/query-service/app/api/license.go
@@ -6,11 +6,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/ee/query-service/constants"
- "github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
"github.com/SigNoz/signoz/ee/query-service/model"
- "github.com/SigNoz/signoz/pkg/http/render"
- "github.com/SigNoz/signoz/pkg/query-service/telemetry"
- "github.com/SigNoz/signoz/pkg/types/authtypes"
)
type DayWiseBreakdown struct {
@@ -49,10 +45,6 @@ type details struct {
BillTotal float64 `json:"billTotal"`
}
-type Redirect struct {
- RedirectURL string `json:"redirectURL"`
-}
-
type billingDetails struct {
Status string `json:"status"`
Data struct {
@@ -64,97 +56,6 @@ type billingDetails struct {
} `json:"data"`
}
-type ApplyLicenseRequest struct {
- LicenseKey string `json:"key"`
-}
-
-func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) {
- ah.listLicensesV2(w, r)
-}
-
-func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) {
- activeLicense, err := ah.LM().GetRepo().GetActiveLicenseV3(r.Context())
- if err != nil {
- RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
- return
- }
-
- // return 404 not found if there is no active license
- if activeLicense == nil {
- RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no active license found")}, nil)
- return
- }
-
- // TODO deprecate this when we move away from key for stripe
- activeLicense.Data["key"] = activeLicense.Key
- render.Success(w, http.StatusOK, activeLicense.Data)
-}
-
-// this function is called by zeus when inserting licenses in the query-service
-func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
- claims, err := authtypes.ClaimsFromContext(r.Context())
- if err != nil {
- render.Error(w, err)
- return
- }
-
- var licenseKey ApplyLicenseRequest
- if err := json.NewDecoder(r.Body).Decode(&licenseKey); err != nil {
- RespondError(w, model.BadRequest(err), nil)
- return
- }
-
- if licenseKey.LicenseKey == "" {
- RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
- return
- }
-
- _, err = ah.LM().ActivateV3(r.Context(), licenseKey.LicenseKey)
- if err != nil {
- telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED, map[string]interface{}{"err": err.Error()}, claims.Email, true, false)
- render.Error(w, err)
- return
- }
-
- render.Success(w, http.StatusAccepted, nil)
-}
-
-func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request) {
- err := ah.LM().RefreshLicense(r.Context())
- if err != nil {
- render.Error(w, err)
- return
- }
-
- render.Success(w, http.StatusNoContent, nil)
-}
-
-func getCheckoutPortalResponse(redirectURL string) *Redirect {
- return &Redirect{RedirectURL: redirectURL}
-}
-
-func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
- checkoutRequest := &model.CheckoutRequest{}
- if err := json.NewDecoder(r.Body).Decode(checkoutRequest); err != nil {
- RespondError(w, model.BadRequest(err), nil)
- return
- }
-
- license := ah.LM().GetActiveLicense()
- if license == nil {
- RespondError(w, model.BadRequestStr("cannot proceed with checkout without license key"), nil)
- return
- }
-
- redirectUrl, err := signozio.CheckoutSession(r.Context(), checkoutRequest, license.Key, ah.Signoz.Zeus)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
-}
-
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
licenseKey := r.URL.Query().Get("licenseKey")
@@ -188,71 +89,3 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
// TODO(srikanthccv):Fetch the current day usage and add it to the response
ah.Respond(w, billingResponse.Data)
}
-
-func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License {
- licensesV2 := []model.License{}
- for _, l := range licenses {
- planKeyFromPlanName, ok := model.MapOldPlanKeyToNewPlanName[l.PlanName]
- if !ok {
- planKeyFromPlanName = model.Basic
- }
- licenseV2 := model.License{
- Key: l.Key,
- ActivationId: "",
- PlanDetails: "",
- FeatureSet: l.Features,
- ValidationMessage: "",
- IsCurrent: l.IsCurrent,
- LicensePlan: model.LicensePlan{
- PlanKey: planKeyFromPlanName,
- ValidFrom: l.ValidFrom,
- ValidUntil: l.ValidUntil,
- Status: l.Status},
- }
- licensesV2 = append(licensesV2, licenseV2)
- }
- return licensesV2
-}
-
-func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
- licensesV3, apierr := ah.LM().GetLicensesV3(r.Context())
- if apierr != nil {
- RespondError(w, apierr, nil)
- return
- }
- licenses := convertLicenseV3ToLicenseV2(licensesV3)
-
- resp := model.Licenses{
- TrialStart: -1,
- TrialEnd: -1,
- OnTrial: false,
- WorkSpaceBlock: false,
- TrialConvertedToSubscription: false,
- GracePeriodEnd: -1,
- Licenses: licenses,
- }
-
- ah.Respond(w, resp)
-}
-
-func (ah *APIHandler) portalSession(w http.ResponseWriter, r *http.Request) {
- portalRequest := &model.PortalRequest{}
- if err := json.NewDecoder(r.Body).Decode(portalRequest); err != nil {
- RespondError(w, model.BadRequest(err), nil)
- return
- }
-
- license := ah.LM().GetActiveLicense()
- if license == nil {
- RespondError(w, model.BadRequestStr("cannot request the portal session without license key"), nil)
- return
- }
-
- redirectUrl, err := signozio.PortalSession(r.Context(), portalRequest, license.Key, ah.Signoz.Zeus)
- if err != nil {
- render.Error(w, err)
- return
- }
-
- ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
-}
diff --git a/ee/query-service/app/api/queryrange.go b/ee/query-service/app/api/queryrange.go
index e6801198e43..698ac2d91c7 100644
--- a/ee/query-service/app/api/queryrange.go
+++ b/ee/query-service/app/api/queryrange.go
@@ -2,11 +2,16 @@ package api
import (
"bytes"
+ "context"
+ "encoding/json"
"fmt"
"io"
"net/http"
+ "runtime/debug"
+ anomalyV2 "github.com/SigNoz/signoz/ee/anomaly"
"github.com/SigNoz/signoz/ee/query-service/anomaly"
+ "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
@@ -15,6 +20,8 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
+
+ qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
@@ -136,3 +143,139 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
aH.QueryRangeV4(w, r)
}
}
+
+func extractSeasonality(anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]) anomalyV2.Seasonality {
+ for _, fn := range anomalyQuery.Functions {
+ if fn.Name == qbtypes.FunctionNameAnomaly {
+ for _, arg := range fn.Args {
+ if arg.Name == "seasonality" {
+ if seasonalityStr, ok := arg.Value.(string); ok {
+ switch seasonalityStr {
+ case "weekly":
+ return anomalyV2.SeasonalityWeekly
+ case "hourly":
+ return anomalyV2.SeasonalityHourly
+ }
+ }
+ }
+ }
+ }
+ }
+ return anomalyV2.SeasonalityDaily // default
+}
+
+func createAnomalyProvider(aH *APIHandler, seasonality anomalyV2.Seasonality) anomalyV2.Provider {
+ switch seasonality {
+ case anomalyV2.SeasonalityWeekly:
+ return anomalyV2.NewWeeklyProvider(
+ anomalyV2.WithQuerier[*anomalyV2.WeeklyProvider](aH.Signoz.Querier),
+ anomalyV2.WithLogger[*anomalyV2.WeeklyProvider](aH.Signoz.Instrumentation.Logger()),
+ )
+ case anomalyV2.SeasonalityHourly:
+ return anomalyV2.NewHourlyProvider(
+ anomalyV2.WithQuerier[*anomalyV2.HourlyProvider](aH.Signoz.Querier),
+ anomalyV2.WithLogger[*anomalyV2.HourlyProvider](aH.Signoz.Instrumentation.Logger()),
+ )
+ default:
+ return anomalyV2.NewDailyProvider(
+ anomalyV2.WithQuerier[*anomalyV2.DailyProvider](aH.Signoz.Querier),
+ anomalyV2.WithLogger[*anomalyV2.DailyProvider](aH.Signoz.Instrumentation.Logger()),
+ )
+ }
+}
+
+func (aH *APIHandler) handleAnomalyQuery(ctx context.Context, orgID valuer.UUID, anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation], queryRangeRequest qbtypes.QueryRangeRequest) (*anomalyV2.AnomaliesResponse, error) {
+ seasonality := extractSeasonality(anomalyQuery)
+ provider := createAnomalyProvider(aH, seasonality)
+
+ return provider.GetAnomalies(ctx, orgID, &anomalyV2.AnomaliesRequest{Params: queryRangeRequest})
+}
+
+func (aH *APIHandler) queryRangeV5(rw http.ResponseWriter, req *http.Request) {
+
+ bodyBytes, err := io.ReadAll(req.Body)
+ if err != nil {
+ render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to read request body: %v", err))
+ return
+ }
+ req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
+
+ ctx := req.Context()
+
+ claims, err := authtypes.ClaimsFromContext(ctx)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ var queryRangeRequest qbtypes.QueryRangeRequest
+ if err := json.NewDecoder(req.Body).Decode(&queryRangeRequest); err != nil {
+ render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to decode request body: %v", err))
+ return
+ }
+
+ defer func() {
+ if r := recover(); r != nil {
+ stackTrace := string(debug.Stack())
+
+ queryJSON, _ := json.Marshal(queryRangeRequest)
+
+ aH.Signoz.Instrumentation.Logger().ErrorContext(ctx, "panic in QueryRange",
+ "error", r,
+ "user", claims.UserID,
+ "payload", string(queryJSON),
+ "stacktrace", stackTrace,
+ )
+
+ render.Error(rw, errors.NewInternalf(
+ errors.CodeInternal,
+ "Something went wrong on our end. It's not you, it's us. Our team is notified about it. Reach out to support if issue persists.",
+ ))
+ }
+ }()
+
+ if err := queryRangeRequest.Validate(); err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ orgID, err := valuer.NewUUID(claims.OrgID)
+ if err != nil {
+ render.Error(rw, err)
+ return
+ }
+
+ if anomalyQuery, ok := queryRangeRequest.IsAnomalyRequest(); ok {
+ anomalies, err := aH.handleAnomalyQuery(ctx, orgID, anomalyQuery, queryRangeRequest)
+ if err != nil {
+ render.Error(rw, errors.NewInternalf(errors.CodeInternal, "failed to get anomalies: %v", err))
+ return
+ }
+
+ results := []any{}
+ for _, item := range anomalies.Results {
+ results = append(results, item)
+ }
+
+ finalResp := &qbtypes.QueryRangeResponse{
+ Type: queryRangeRequest.RequestType,
+ Data: struct {
+ Results []any `json:"results"`
+ }{
+ Results: results,
+ },
+ Meta: struct {
+ RowsScanned uint64 `json:"rowsScanned"`
+ BytesScanned uint64 `json:"bytesScanned"`
+ DurationMS uint64 `json:"durationMs"`
+ }{},
+ }
+
+ render.Success(rw, http.StatusOK, finalResp)
+ return
+ } else {
+ // regular query range request, let the querier handle it
+ req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
+ aH.QuerierAPI.QueryRange(rw, req)
+ }
+}
diff --git a/ee/query-service/app/db/reader.go b/ee/query-service/app/db/reader.go
deleted file mode 100644
index c518f5042d3..00000000000
--- a/ee/query-service/app/db/reader.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package db
-
-import (
- "time"
-
- "github.com/ClickHouse/clickhouse-go/v2"
-
- "github.com/SigNoz/signoz/pkg/cache"
- "github.com/SigNoz/signoz/pkg/prometheus"
- basechr "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
- "github.com/SigNoz/signoz/pkg/sqlstore"
- "github.com/SigNoz/signoz/pkg/telemetrystore"
-)
-
-type ClickhouseReader struct {
- conn clickhouse.Conn
- appdb sqlstore.SQLStore
- *basechr.ClickHouseReader
-}
-
-func NewDataConnector(
- sqlDB sqlstore.SQLStore,
- telemetryStore telemetrystore.TelemetryStore,
- prometheus prometheus.Prometheus,
- cluster string,
- fluxIntervalForTraceDetail time.Duration,
- cache cache.Cache,
-) *ClickhouseReader {
- chReader := basechr.NewReader(sqlDB, telemetryStore, prometheus, cluster, fluxIntervalForTraceDetail, cache)
- return &ClickhouseReader{
- conn: telemetryStore.ClickhouseDB(),
- appdb: sqlDB,
- ClickHouseReader: chReader,
- }
-}
-
-func (r *ClickhouseReader) GetSQLStore() sqlstore.SQLStore {
- return r.appdb
-}
diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go
index fb94661b31c..dab2b7f5163 100644
--- a/ee/query-service/app/server.go
+++ b/ee/query-service/app/server.go
@@ -3,25 +3,25 @@ package app
import (
"context"
"fmt"
+ "log/slog"
"net"
"net/http"
_ "net/http/pprof" // http profiler
- "time"
+
+ "github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
"github.com/gorilla/handlers"
- "github.com/jmoiron/sqlx"
- eemiddleware "github.com/SigNoz/signoz/ee/http/middleware"
"github.com/SigNoz/signoz/ee/query-service/app/api"
- "github.com/SigNoz/signoz/ee/query-service/app/db"
- "github.com/SigNoz/signoz/ee/query-service/constants"
- "github.com/SigNoz/signoz/ee/query-service/dao/sqlite"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/ee/query-service/rules"
+ "github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
+ "github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/prometheus"
+ "github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
@@ -30,11 +30,9 @@ import (
"github.com/rs/cors"
"github.com/soheilhy/cmux"
- licensepkg "github.com/SigNoz/signoz/ee/query-service/license"
- "github.com/SigNoz/signoz/ee/query-service/usage"
-
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
+ "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
@@ -44,86 +42,56 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
- "github.com/SigNoz/signoz/pkg/query-service/telemetry"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"go.uber.org/zap"
)
-type ServerOptions struct {
- Config signoz.Config
- SigNoz *signoz.SigNoz
- HTTPHostPort string
- PrivateHostPort string
- PreferSpanMetrics bool
- FluxInterval string
- FluxIntervalForTraceDetail string
- Cluster string
- GatewayUrl string
- Jwt *authtypes.JWT
-}
-
-// Server runs HTTP api service
+// Server runs HTTP, Mux and a grpc server
type Server struct {
- serverOptions *ServerOptions
- ruleManager *baserules.Manager
+ config signoz.Config
+ signoz *signoz.SigNoz
+ jwt *authtypes.JWT
+ ruleManager *baserules.Manager
// public http router
- httpConn net.Listener
- httpServer *http.Server
+ httpConn net.Listener
+ httpServer *http.Server
+ httpHostPort string
- // private http
- privateConn net.Listener
- privateHTTP *http.Server
+ opampServer *opamp.Server
// Usage manager
usageManager *usage.Manager
- opampServer *opamp.Server
-
unavailableChannel chan healthcheck.Status
}
-// HealthCheckStatus returns health check status channel a client can subscribe to
-func (s Server) HealthCheckStatus() chan healthcheck.Status {
- return s.unavailableChannel
-}
-
// NewServer creates and initializes Server
-func NewServer(serverOptions *ServerOptions) (*Server, error) {
- modelDao := sqlite.NewModelDao(serverOptions.SigNoz.SQLStore)
- gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
- if err != nil {
- return nil, err
- }
-
- // initiate license manager
- lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB(), serverOptions.SigNoz.SQLStore, serverOptions.SigNoz.Zeus)
- if err != nil {
- return nil, err
- }
-
- fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
+func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) (*Server, error) {
+ gatewayProxy, err := gateway.NewProxy(config.Gateway.URL.String(), gateway.RoutePrefix)
if err != nil {
return nil, err
}
- reader := db.NewDataConnector(
- serverOptions.SigNoz.SQLStore,
- serverOptions.SigNoz.TelemetryStore,
- serverOptions.SigNoz.Prometheus,
- serverOptions.Cluster,
- fluxIntervalForTraceDetail,
- serverOptions.SigNoz.Cache,
+ reader := clickhouseReader.NewReader(
+ signoz.SQLStore,
+ signoz.TelemetryStore,
+ signoz.Prometheus,
+ signoz.TelemetryStore.Cluster(),
+ config.Querier.FluxInterval,
+ signoz.Cache,
)
rm, err := makeRulesManager(
- serverOptions.SigNoz.SQLStore.SQLxDB(),
reader,
- serverOptions.SigNoz.Cache,
- serverOptions.SigNoz.Alertmanager,
- serverOptions.SigNoz.SQLStore,
- serverOptions.SigNoz.TelemetryStore,
- serverOptions.SigNoz.Prometheus,
+ signoz.Cache,
+ signoz.Alertmanager,
+ signoz.SQLStore,
+ signoz.TelemetryStore,
+ signoz.Prometheus,
+ signoz.Modules.OrgGetter,
+ signoz.Querier,
+ signoz.Instrumentation.Logger(),
)
if err != nil {
@@ -131,19 +99,16 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate opamp
- _, err = opAmpModel.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB())
- if err != nil {
- return nil, err
- }
+ opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter)
- integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
+ integrationsController, err := integrations.NewController(signoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
- cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
+ cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
@@ -152,7 +117,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
// ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
- serverOptions.SigNoz.SQLStore, integrationsController.GetPipelinesForInstalledIntegrations,
+ signoz.SQLStore,
+ integrationsController.GetPipelinesForInstalledIntegrations,
)
if err != nil {
return nil, err
@@ -160,7 +126,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
// initiate agent config handler
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
- DB: serverOptions.SigNoz.SQLStore.SQLxDB(),
+ Store: signoz.SQLStore,
AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController},
})
if err != nil {
@@ -168,59 +134,44 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// start the usagemanager
- usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus)
+ usageManager, err := usage.New(signoz.Licensing, signoz.TelemetryStore.ClickhouseDB(), signoz.Zeus, signoz.Modules.OrgGetter)
if err != nil {
return nil, err
}
- err = usageManager.Start()
- if err != nil {
- return nil, err
- }
-
- telemetry.GetInstance().SetReader(reader)
- telemetry.GetInstance().SetSqlStore(serverOptions.SigNoz.SQLStore)
- telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
- telemetry.GetInstance().SetSavedViewsInfoCallback(telemetry.GetSavedViewsInfo)
- telemetry.GetInstance().SetAlertsInfoCallback(telemetry.GetAlertsInfo)
- telemetry.GetInstance().SetGetUsersCallback(telemetry.GetUsers)
- telemetry.GetInstance().SetUserCountCallback(telemetry.GetUserCount)
- telemetry.GetInstance().SetDashboardsInfoCallback(telemetry.GetDashboardsInfo)
-
- fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
+ err = usageManager.Start(context.Background())
if err != nil {
return nil, err
}
apiOpts := api.APIHandlerOptions{
DataConnector: reader,
- PreferSpanMetrics: serverOptions.PreferSpanMetrics,
- AppDao: modelDao,
RulesManager: rm,
UsageManager: usageManager,
- FeatureFlags: lm,
- LicenseManager: lm,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
- FluxInterval: fluxInterval,
+ FluxInterval: config.Querier.FluxInterval,
Gateway: gatewayProxy,
- GatewayUrl: serverOptions.GatewayUrl,
- JWT: serverOptions.Jwt,
+ GatewayUrl: config.Gateway.URL.String(),
+ JWT: jwt,
}
- apiHandler, err := api.NewAPIHandler(apiOpts, serverOptions.SigNoz)
+ apiHandler, err := api.NewAPIHandler(apiOpts, signoz)
if err != nil {
return nil, err
}
s := &Server{
+ config: config,
+ signoz: signoz,
+ jwt: jwt,
ruleManager: rm,
- serverOptions: serverOptions,
+ httpHostPort: baseconst.HTTPHostPort,
unavailableChannel: make(chan healthcheck.Status),
usageManager: usageManager,
}
- httpServer, err := s.createPublicServer(apiHandler, serverOptions.SigNoz.Web)
+ httpServer, err := s.createPublicServer(apiHandler, signoz.Web)
if err != nil {
return nil, err
@@ -228,76 +179,31 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
s.httpServer = httpServer
- privateServer, err := s.createPrivateServer(apiHandler)
- if err != nil {
- return nil, err
- }
-
- s.privateHTTP = privateServer
-
s.opampServer = opamp.InitializeServer(
- &opAmpModel.AllAgents, agentConfMgr,
+ &opAmpModel.AllAgents, agentConfMgr, signoz.Instrumentation,
)
- orgs, err := apiHandler.Signoz.Modules.Organization.GetAll(context.Background())
- if err != nil {
- return nil, err
- }
- for _, org := range orgs {
- errorList := reader.PreloadMetricsMetadata(context.Background(), org.ID)
- for _, er := range errorList {
- zap.L().Error("failed to preload metrics metadata", zap.Error(er))
- }
- }
-
return s, nil
}
-func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
-
- r := baseapp.NewRouter()
-
- r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
- r.Use(eemiddleware.NewPat(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}).Wrap)
- r.Use(middleware.NewTimeout(zap.L(),
- s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
- s.serverOptions.Config.APIServer.Timeout.Default,
- s.serverOptions.Config.APIServer.Timeout.Max,
- ).Wrap)
- r.Use(middleware.NewAnalytics(zap.L()).Wrap)
- r.Use(middleware.NewLogging(zap.L(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
-
- apiHandler.RegisterPrivateRoutes(r)
-
- c := cors.New(cors.Options{
- //todo(amol): find out a way to add exact domain or
- // ip here for alert manager
- AllowedOrigins: []string{"*"},
- AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"},
- AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
- })
-
- handler := c.Handler(r)
- handler = handlers.CompressHandler(handler)
-
- return &http.Server{
- Handler: handler,
- }, nil
+// HealthCheckStatus returns health check status channel a client can subscribe to
+func (s Server) HealthCheckStatus() chan healthcheck.Status {
+ return s.unavailableChannel
}
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) {
r := baseapp.NewRouter()
- am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
-
- r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
- r.Use(eemiddleware.NewPat(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}).Wrap)
- r.Use(middleware.NewTimeout(zap.L(),
- s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
- s.serverOptions.Config.APIServer.Timeout.Default,
- s.serverOptions.Config.APIServer.Timeout.Max,
+ am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())
+
+ r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
+ r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
+ r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
+ s.config.APIServer.Timeout.ExcludedRoutes,
+ s.config.APIServer.Timeout.Default,
+ s.config.APIServer.Timeout.Max,
).Wrap)
- r.Use(middleware.NewAnalytics(zap.L()).Wrap)
- r.Use(middleware.NewLogging(zap.L(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
+ r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
+ r.Use(middleware.NewComment().Wrap)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
@@ -307,10 +213,12 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
+ apiHandler.RegisterQueryRangeV5Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am)
+ apiHandler.RegisterTraceFunnelsRoutes(r, am)
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
@@ -336,7 +244,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
func (s *Server) initListeners() error {
// listen on public port
var err error
- publicHostPort := s.serverOptions.HTTPHostPort
+ publicHostPort := s.httpHostPort
if publicHostPort == "" {
return fmt.Errorf("baseconst.HTTPHostPort is required")
}
@@ -346,20 +254,7 @@ func (s *Server) initListeners() error {
return err
}
- zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort))
-
- // listen on private port to support internal services
- privateHostPort := s.serverOptions.PrivateHostPort
-
- if privateHostPort == "" {
- return fmt.Errorf("baseconst.PrivateHostPort is required")
- }
-
- s.privateConn, err = net.Listen("tcp", privateHostPort)
- if err != nil {
- return err
- }
- zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort))
+ zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
return nil
}
@@ -379,7 +274,7 @@ func (s *Server) Start(ctx context.Context) error {
}
go func() {
- zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort))
+ zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.httpHostPort))
switch err := s.httpServer.Serve(s.httpConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
@@ -399,26 +294,6 @@ func (s *Server) Start(ctx context.Context) error {
}
}()
- var privatePort int
- if port, err := utils.GetPort(s.privateConn.Addr()); err == nil {
- privatePort = port
- }
-
- go func() {
- zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
-
- switch err := s.privateHTTP.Serve(s.privateConn); err {
- case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
- // normal exit, nothing to do
- zap.L().Info("private http server closed")
- default:
- zap.L().Error("Could not start private HTTP server", zap.Error(err))
- }
-
- s.unavailableChannel <- healthcheck.Unavailable
-
- }()
-
go func() {
zap.L().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint))
err := s.opampServer.Start(baseconst.OpAmpWsEndpoint)
@@ -431,15 +306,9 @@ func (s *Server) Start(ctx context.Context) error {
return nil
}
-func (s *Server) Stop() error {
+func (s *Server) Stop(ctx context.Context) error {
if s.httpServer != nil {
- if err := s.httpServer.Shutdown(context.Background()); err != nil {
- return err
- }
- }
-
- if s.privateHTTP != nil {
- if err := s.privateHTTP.Shutdown(context.Background()); err != nil {
+ if err := s.httpServer.Shutdown(ctx); err != nil {
return err
}
}
@@ -447,38 +316,36 @@ func (s *Server) Stop() error {
s.opampServer.Stop()
if s.ruleManager != nil {
- s.ruleManager.Stop(context.Background())
+ s.ruleManager.Stop(ctx)
}
// stop usage manager
- s.usageManager.Stop()
+ s.usageManager.Stop(ctx)
return nil
}
-func makeRulesManager(
- db *sqlx.DB,
- ch baseint.Reader,
- cache cache.Cache,
- alertmanager alertmanager.Alertmanager,
- sqlstore sqlstore.SQLStore,
- telemetryStore telemetrystore.TelemetryStore,
- prometheus prometheus.Prometheus,
-) (*baserules.Manager, error) {
+func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, logger *slog.Logger) (*baserules.Manager, error) {
+ ruleStore := sqlrulestore.NewRuleStore(sqlstore)
+ maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
// create manager opts
managerOpts := &baserules.ManagerOptions{
TelemetryStore: telemetryStore,
Prometheus: prometheus,
- DBConn: db,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,
+ Querier: querier,
+ SLogger: logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager,
- SQLStore: sqlstore,
+ OrgGetter: orgGetter,
+ RuleStore: ruleStore,
+ MaintenanceStore: maintenanceStore,
+ SqlStore: sqlstore,
}
// create Manager
diff --git a/ee/query-service/constants/constants.go b/ee/query-service/constants/constants.go
index 701495c6eb0..b7c6d11fc58 100644
--- a/ee/query-service/constants/constants.go
+++ b/ee/query-service/constants/constants.go
@@ -33,3 +33,18 @@ func GetOrDefaultEnv(key string, fallback string) string {
func GetDefaultSiteURL() string {
return GetOrDefaultEnv("SIGNOZ_SITE_URL", DefaultSiteURL)
}
+
+const DotMetricsEnabled = "DOT_METRICS_ENABLED"
+
+var IsDotMetricsEnabled = false
+var IsPreferSpanMetrics = false
+
+func init() {
+ if GetOrDefaultEnv(DotMetricsEnabled, "true") == "true" {
+ IsDotMetricsEnabled = true
+ }
+
+ if GetOrDefaultEnv("USE_SPAN_METRICS", "false") == "true" {
+ IsPreferSpanMetrics = true
+ }
+}
diff --git a/ee/query-service/dao/interface.go b/ee/query-service/dao/interface.go
deleted file mode 100644
index 2e40abcf216..00000000000
--- a/ee/query-service/dao/interface.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package dao
-
-import (
- "context"
- "net/url"
-
- basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
- "github.com/SigNoz/signoz/pkg/types"
- "github.com/google/uuid"
-)
-
-type ModelDao interface {
- // auth methods
- GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*types.GettableOrgDomain, error)
-
- // org domain (auth domains) CRUD ops
- ListDomains(ctx context.Context, orgId string) ([]types.GettableOrgDomain, basemodel.BaseApiError)
- GetDomain(ctx context.Context, id uuid.UUID) (*types.GettableOrgDomain, basemodel.BaseApiError)
- CreateDomain(ctx context.Context, d *types.GettableOrgDomain) basemodel.BaseApiError
- UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) basemodel.BaseApiError
- DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError
- GetDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, basemodel.BaseApiError)
-}
diff --git a/ee/query-service/dao/sqlite/domain.go b/ee/query-service/dao/sqlite/domain.go
deleted file mode 100644
index d41f7632eaa..00000000000
--- a/ee/query-service/dao/sqlite/domain.go
+++ /dev/null
@@ -1,272 +0,0 @@
-package sqlite
-
-import (
- "context"
- "database/sql"
- "encoding/json"
- "fmt"
- "net/url"
- "strings"
- "time"
-
- "github.com/SigNoz/signoz/ee/query-service/model"
- basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
- "github.com/SigNoz/signoz/pkg/types"
- ossTypes "github.com/SigNoz/signoz/pkg/types"
- "github.com/google/uuid"
- "go.uber.org/zap"
-)
-
-// GetDomainFromSsoResponse uses relay state received from IdP to fetch
-// user domain. The domain is further used to process validity of the response.
-// when sending login request to IdP we send relay state as URL (site url)
-// with domainId or domainName as query parameter.
-func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*types.GettableOrgDomain, error) {
- // derive domain id from relay state now
- var domainIdStr string
- var domainNameStr string
- var domain *types.GettableOrgDomain
-
- for k, v := range relayState.Query() {
- if k == "domainId" && len(v) > 0 {
- domainIdStr = strings.Replace(v[0], ":", "-", -1)
- }
- if k == "domainName" && len(v) > 0 {
- domainNameStr = v[0]
- }
- }
-
- if domainIdStr != "" {
- domainId, err := uuid.Parse(domainIdStr)
- if err != nil {
- zap.L().Error("failed to parse domainId from relay state", zap.Error(err))
- return nil, fmt.Errorf("failed to parse domainId from IdP response")
- }
-
- domain, err = m.GetDomain(ctx, domainId)
- if err != nil {
- zap.L().Error("failed to find domain from domainId received in IdP response", zap.Error(err))
- return nil, fmt.Errorf("invalid credentials")
- }
- }
-
- if domainNameStr != "" {
-
- domainFromDB, err := m.GetDomainByName(ctx, domainNameStr)
- domain = domainFromDB
- if err != nil {
- zap.L().Error("failed to find domain from domainName received in IdP response", zap.Error(err))
- return nil, fmt.Errorf("invalid credentials")
- }
- }
- if domain != nil {
- return domain, nil
- }
-
- return nil, fmt.Errorf("failed to find domain received in IdP response")
-}
-
-// GetDomainByName returns org domain for a given domain name
-func (m *modelDao) GetDomainByName(ctx context.Context, name string) (*types.GettableOrgDomain, basemodel.BaseApiError) {
-
- stored := types.StorableOrgDomain{}
- err := m.sqlStore.BunDB().NewSelect().
- Model(&stored).
- Where("name = ?", name).
- Limit(1).
- Scan(ctx)
-
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, model.BadRequest(fmt.Errorf("invalid domain name"))
- }
- return nil, model.InternalError(err)
- }
-
- domain := &types.GettableOrgDomain{StorableOrgDomain: stored}
- if err := domain.LoadConfig(stored.Data); err != nil {
- return nil, model.InternalError(err)
- }
- return domain, nil
-}
-
-// GetDomain returns org domain for a given domain id
-func (m *modelDao) GetDomain(ctx context.Context, id uuid.UUID) (*types.GettableOrgDomain, basemodel.BaseApiError) {
-
- stored := types.StorableOrgDomain{}
- err := m.sqlStore.BunDB().NewSelect().
- Model(&stored).
- Where("id = ?", id).
- Limit(1).
- Scan(ctx)
-
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, model.BadRequest(fmt.Errorf("invalid domain id"))
- }
- return nil, model.InternalError(err)
- }
-
- domain := &types.GettableOrgDomain{StorableOrgDomain: stored}
- if err := domain.LoadConfig(stored.Data); err != nil {
- return nil, model.InternalError(err)
- }
- return domain, nil
-}
-
-// ListDomains gets the list of auth domains by org id
-func (m *modelDao) ListDomains(ctx context.Context, orgId string) ([]types.GettableOrgDomain, basemodel.BaseApiError) {
- domains := []types.GettableOrgDomain{}
-
- stored := []types.StorableOrgDomain{}
- err := m.sqlStore.BunDB().NewSelect().
- Model(&stored).
- Where("org_id = ?", orgId).
- Scan(ctx)
-
- if err != nil {
- if err == sql.ErrNoRows {
- return domains, nil
- }
- return nil, model.InternalError(err)
- }
-
- for _, s := range stored {
- domain := types.GettableOrgDomain{StorableOrgDomain: s}
- if err := domain.LoadConfig(s.Data); err != nil {
- zap.L().Error("ListDomains() failed", zap.Error(err))
- }
- domains = append(domains, domain)
- }
-
- return domains, nil
-}
-
-// CreateDomain creates a new auth domain
-func (m *modelDao) CreateDomain(ctx context.Context, domain *types.GettableOrgDomain) basemodel.BaseApiError {
-
- if domain.ID == uuid.Nil {
- domain.ID = uuid.New()
- }
-
- if domain.OrgID == "" || domain.Name == "" {
- return model.BadRequest(fmt.Errorf("domain creation failed, missing fields: OrgID, Name "))
- }
-
- configJson, err := json.Marshal(domain)
- if err != nil {
- zap.L().Error("failed to unmarshal domain config", zap.Error(err))
- return model.InternalError(fmt.Errorf("domain creation failed"))
- }
-
- storableDomain := types.StorableOrgDomain{
- ID: domain.ID,
- Name: domain.Name,
- OrgID: domain.OrgID,
- Data: string(configJson),
- TimeAuditable: ossTypes.TimeAuditable{CreatedAt: time.Now(), UpdatedAt: time.Now()},
- }
-
- _, err = m.sqlStore.BunDB().NewInsert().
- Model(&storableDomain).
- Exec(ctx)
-
- if err != nil {
- zap.L().Error("failed to insert domain in db", zap.Error(err))
- return model.InternalError(fmt.Errorf("domain creation failed"))
- }
-
- return nil
-}
-
-// UpdateDomain updates stored config params for a domain
-func (m *modelDao) UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) basemodel.BaseApiError {
-
- if domain.ID == uuid.Nil {
- zap.L().Error("domain update failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
- return model.InternalError(fmt.Errorf("domain update failed"))
- }
-
- configJson, err := json.Marshal(domain)
- if err != nil {
- zap.L().Error("domain update failed", zap.Error(err))
- return model.InternalError(fmt.Errorf("domain update failed"))
- }
-
- storableDomain := &types.StorableOrgDomain{
- ID: domain.ID,
- Name: domain.Name,
- OrgID: domain.OrgID,
- Data: string(configJson),
- TimeAuditable: ossTypes.TimeAuditable{UpdatedAt: time.Now()},
- }
-
- _, err = m.sqlStore.BunDB().NewUpdate().
- Model(storableDomain).
- Column("data", "updated_at").
- WherePK().
- Exec(ctx)
-
- if err != nil {
- zap.L().Error("domain update failed", zap.Error(err))
- return model.InternalError(fmt.Errorf("domain update failed"))
- }
-
- return nil
-}
-
-// DeleteDomain deletes an org domain
-func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError {
-
- if id == uuid.Nil {
- zap.L().Error("domain delete failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
- return model.InternalError(fmt.Errorf("domain delete failed"))
- }
-
- storableDomain := &types.StorableOrgDomain{ID: id}
- _, err := m.sqlStore.BunDB().NewDelete().
- Model(storableDomain).
- WherePK().
- Exec(ctx)
-
- if err != nil {
- zap.L().Error("domain delete failed", zap.Error(err))
- return model.InternalError(fmt.Errorf("domain delete failed"))
- }
-
- return nil
-}
-
-func (m *modelDao) GetDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, basemodel.BaseApiError) {
-
- if email == "" {
- return nil, model.BadRequest(fmt.Errorf("could not find auth domain, missing fields: email "))
- }
-
- components := strings.Split(email, "@")
- if len(components) < 2 {
- return nil, model.BadRequest(fmt.Errorf("invalid email address"))
- }
-
- parsedDomain := components[1]
-
- stored := types.StorableOrgDomain{}
- err := m.sqlStore.BunDB().NewSelect().
- Model(&stored).
- Where("name = ?", parsedDomain).
- Limit(1).
- Scan(ctx)
-
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, nil
- }
- return nil, model.InternalError(err)
- }
-
- domain := &types.GettableOrgDomain{StorableOrgDomain: stored}
- if err := domain.LoadConfig(stored.Data); err != nil {
- return nil, model.InternalError(err)
- }
- return domain, nil
-}
diff --git a/ee/query-service/dao/sqlite/modelDao.go b/ee/query-service/dao/sqlite/modelDao.go
deleted file mode 100644
index 6e55649e996..00000000000
--- a/ee/query-service/dao/sqlite/modelDao.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package sqlite
-
-import (
- "github.com/SigNoz/signoz/pkg/modules/user"
- "github.com/SigNoz/signoz/pkg/modules/user/impluser"
- "github.com/SigNoz/signoz/pkg/sqlstore"
-)
-
-type modelDao struct {
- userModule user.Module
- sqlStore sqlstore.SQLStore
-}
-
-// InitDB creates and extends base model DB repository
-func NewModelDao(sqlStore sqlstore.SQLStore) *modelDao {
- userModule := impluser.NewModule(impluser.NewStore(sqlStore))
- return &modelDao{userModule: userModule, sqlStore: sqlStore}
-}
diff --git a/ee/query-service/integrations/signozio/signozio.go b/ee/query-service/integrations/signozio/signozio.go
deleted file mode 100644
index d1bd7657284..00000000000
--- a/ee/query-service/integrations/signozio/signozio.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package signozio
-
-import (
- "context"
- "encoding/json"
-
- "github.com/SigNoz/signoz/ee/query-service/model"
- "github.com/SigNoz/signoz/pkg/zeus"
- "github.com/tidwall/gjson"
-)
-
-func ValidateLicenseV3(ctx context.Context, licenseKey string, zeus zeus.Zeus) (*model.LicenseV3, error) {
- data, err := zeus.GetLicense(ctx, licenseKey)
- if err != nil {
- return nil, err
- }
-
- var m map[string]any
- if err = json.Unmarshal(data, &m); err != nil {
- return nil, err
- }
-
- license, err := model.NewLicenseV3(m)
- if err != nil {
- return nil, err
- }
-
- return license, nil
-}
-
-// SendUsage reports the usage of signoz to license server
-func SendUsage(ctx context.Context, usage model.UsagePayload, zeus zeus.Zeus) error {
- body, err := json.Marshal(usage)
- if err != nil {
- return err
- }
-
- return zeus.PutMeters(ctx, usage.LicenseKey.String(), body)
-}
-
-func CheckoutSession(ctx context.Context, checkoutRequest *model.CheckoutRequest, licenseKey string, zeus zeus.Zeus) (string, error) {
- body, err := json.Marshal(checkoutRequest)
- if err != nil {
- return "", err
- }
-
- response, err := zeus.GetCheckoutURL(ctx, licenseKey, body)
- if err != nil {
- return "", err
- }
-
- return gjson.GetBytes(response, "url").String(), nil
-}
-
-func PortalSession(ctx context.Context, portalRequest *model.PortalRequest, licenseKey string, zeus zeus.Zeus) (string, error) {
- body, err := json.Marshal(portalRequest)
- if err != nil {
- return "", err
- }
-
- response, err := zeus.GetPortalURL(ctx, licenseKey, body)
- if err != nil {
- return "", err
- }
-
- return gjson.GetBytes(response, "url").String(), nil
-}
diff --git a/ee/query-service/interfaces/connector.go b/ee/query-service/interfaces/connector.go
deleted file mode 100644
index 7571000daf2..00000000000
--- a/ee/query-service/interfaces/connector.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package interfaces
-
-import (
- baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
-)
-
-// Connector defines methods for interaction
-// with o11y data. for example - clickhouse
-type DataConnector interface {
- baseint.Reader
-}
diff --git a/ee/query-service/license/db.go b/ee/query-service/license/db.go
deleted file mode 100644
index c241ad87662..00000000000
--- a/ee/query-service/license/db.go
+++ /dev/null
@@ -1,248 +0,0 @@
-package license
-
-import (
- "context"
- "database/sql"
- "encoding/json"
- "fmt"
- "time"
-
- "github.com/jmoiron/sqlx"
- "github.com/mattn/go-sqlite3"
-
- "github.com/SigNoz/signoz/ee/query-service/model"
- basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
- "github.com/SigNoz/signoz/pkg/sqlstore"
- "github.com/SigNoz/signoz/pkg/types"
- "go.uber.org/zap"
-)
-
-// Repo is license repo. stores license keys in a secured DB
-type Repo struct {
- db *sqlx.DB
- store sqlstore.SQLStore
-}
-
-// NewLicenseRepo initiates a new license repo
-func NewLicenseRepo(db *sqlx.DB, store sqlstore.SQLStore) Repo {
- return Repo{
- db: db,
- store: store,
- }
-}
-
-func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
- licensesData := []model.LicenseDB{}
- licenseV3Data := []*model.LicenseV3{}
-
- query := "SELECT id,key,data FROM licenses_v3"
-
- err := r.db.Select(&licensesData, query)
- if err != nil {
- return nil, fmt.Errorf("failed to get licenses from db: %v", err)
- }
-
- for _, l := range licensesData {
- var licenseData map[string]interface{}
- err := json.Unmarshal([]byte(l.Data), &licenseData)
- if err != nil {
- return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
- }
-
- license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
- if err != nil {
- return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
- }
- licenseV3Data = append(licenseV3Data, license)
- }
-
- return licenseV3Data, nil
-}
-
-// GetActiveLicense fetches the latest active license from DB.
-// If the license is not present, expect a nil license and a nil error in the output.
-func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
- activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
- if err != nil {
- return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
- }
-
- if activeLicenseV3 == nil {
- return nil, nil
- }
- activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
- return activeLicenseV2, nil
-}
-
-func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
- var err error
- licenses := []model.LicenseDB{}
-
- query := "SELECT id,key,data FROM licenses_v3"
-
- err = r.db.Select(&licenses, query)
- if err != nil {
- return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
- }
-
- var active *model.LicenseV3
- for _, l := range licenses {
- var licenseData map[string]interface{}
- err := json.Unmarshal([]byte(l.Data), &licenseData)
- if err != nil {
- return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
- }
-
- license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
- if err != nil {
- return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
- }
-
- if active == nil &&
- (license.ValidFrom != 0) &&
- (license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
- active = license
- }
- if active != nil &&
- license.ValidFrom > active.ValidFrom &&
- (license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
- active = license
- }
- }
-
- return active, nil
-}
-
-// InsertLicenseV3 inserts a new license v3 in db
-func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
-
- query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
-
- // licsense is the entity of zeus so putting the entire license here without defining schema
- licenseData, err := json.Marshal(l.Data)
- if err != nil {
- return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
- }
-
- _, err = r.db.ExecContext(ctx,
- query,
- l.ID,
- l.Key,
- string(licenseData),
- )
-
- if err != nil {
- if sqliteErr, ok := err.(sqlite3.Error); ok {
- if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
- zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
- return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
- }
- }
- zap.L().Error("error in inserting license data: ", zap.Error(err))
- return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
- }
-
- return nil
-}
-
-// UpdateLicenseV3 updates a new license v3 in db
-func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
-
- // the key and id for the license can't change so only update the data here!
- query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
-
- license, err := json.Marshal(l.Data)
- if err != nil {
- return fmt.Errorf("insert license failed: license marshal error")
- }
- _, err = r.db.ExecContext(ctx,
- query,
- license,
- l.ID,
- )
-
- if err != nil {
- zap.L().Error("error in updating license data: ", zap.Error(err))
- return fmt.Errorf("failed to update license in db: %v", err)
- }
-
- return nil
-}
-
-func (r *Repo) CreateFeature(req *types.FeatureStatus) *basemodel.ApiError {
-
- _, err := r.store.BunDB().NewInsert().
- Model(req).
- Exec(context.Background())
- if err != nil {
- return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err}
- }
- return nil
-}
-
-func (r *Repo) GetFeature(featureName string) (types.FeatureStatus, error) {
- var feature types.FeatureStatus
-
- err := r.store.BunDB().NewSelect().
- Model(&feature).
- Where("name = ?", featureName).
- Scan(context.Background())
-
- if err != nil {
- return feature, err
- }
- if feature.Name == "" {
- return feature, basemodel.ErrFeatureUnavailable{Key: featureName}
- }
- return feature, nil
-}
-
-func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) {
-
- var feature []basemodel.Feature
-
- err := r.db.Select(&feature,
- `SELECT * FROM feature_status;`)
- if err != nil {
- return feature, err
- }
-
- return feature, nil
-}
-
-func (r *Repo) UpdateFeature(req types.FeatureStatus) error {
-
- _, err := r.store.BunDB().NewUpdate().
- Model(&req).
- Where("name = ?", req.Name).
- Exec(context.Background())
- if err != nil {
- return err
- }
- return nil
-}
-
-func (r *Repo) InitFeatures(req []types.FeatureStatus) error {
- // get a feature by name, if it doesn't exist, create it. If it does exist, update it.
- for _, feature := range req {
- currentFeature, err := r.GetFeature(feature.Name)
- if err != nil && err == sql.ErrNoRows {
- err := r.CreateFeature(&feature)
- if err != nil {
- return err
- }
- continue
- } else if err != nil {
- return err
- }
- feature.Usage = int(currentFeature.Usage)
- if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 {
- feature.Active = false
- }
- err = r.UpdateFeature(feature)
- if err != nil {
- return err
- }
- }
- return nil
-}
diff --git a/ee/query-service/license/manager.go b/ee/query-service/license/manager.go
deleted file mode 100644
index ae5b5c89790..00000000000
--- a/ee/query-service/license/manager.go
+++ /dev/null
@@ -1,318 +0,0 @@
-package license
-
-import (
- "context"
- "sync/atomic"
- "time"
-
- "github.com/jmoiron/sqlx"
-
- "sync"
-
- baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
- "github.com/SigNoz/signoz/pkg/sqlstore"
- "github.com/SigNoz/signoz/pkg/types"
- "github.com/SigNoz/signoz/pkg/zeus"
-
- validate "github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
- "github.com/SigNoz/signoz/ee/query-service/model"
- basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
- "github.com/SigNoz/signoz/pkg/query-service/telemetry"
- "go.uber.org/zap"
-)
-
-var LM *Manager
-
-// validate and update license every 24 hours
-var validationFrequency = 24 * 60 * time.Minute
-
-type Manager struct {
- repo *Repo
- zeus zeus.Zeus
- mutex sync.Mutex
- validatorRunning bool
- // end the license validation, this is important to gracefully
- // stopping validation and protect in-consistent updates
- done chan struct{}
- // terminated waits for the validate go routine to end
- terminated chan struct{}
- // last time the license was validated
- lastValidated int64
- // keep track of validation failure attempts
- failedAttempts uint64
- // keep track of active license and features
- activeLicenseV3 *model.LicenseV3
- activeFeatures basemodel.FeatureSet
-}
-
-func StartManager(db *sqlx.DB, store sqlstore.SQLStore, zeus zeus.Zeus, features ...basemodel.Feature) (*Manager, error) {
- if LM != nil {
- return LM, nil
- }
-
- repo := NewLicenseRepo(db, store)
- m := &Manager{
- repo: &repo,
- zeus: zeus,
- }
- if err := m.start(features...); err != nil {
- return m, err
- }
-
- LM = m
- return m, nil
-}
-
-// start loads active license in memory and initiates validator
-func (lm *Manager) start(features ...basemodel.Feature) error {
- return lm.LoadActiveLicenseV3(features...)
-}
-
-func (lm *Manager) Stop() {
- close(lm.done)
- <-lm.terminated
-}
-
-func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
- lm.mutex.Lock()
- defer lm.mutex.Unlock()
-
- if l == nil {
- return
- }
-
- lm.activeLicenseV3 = l
- lm.activeFeatures = append(l.Features, features...)
- // set default features
- setDefaultFeatures(lm)
-
- err := lm.InitFeatures(lm.activeFeatures)
- if err != nil {
- zap.L().Panic("Couldn't activate features", zap.Error(err))
- }
- if !lm.validatorRunning {
- // we want to make sure only one validator runs,
- // we already have lock() so good to go
- lm.validatorRunning = true
- go lm.ValidatorV3(context.Background())
- }
-
-}
-
-func setDefaultFeatures(lm *Manager) {
- lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
-}
-
-func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
- active, err := lm.repo.GetActiveLicenseV3(context.Background())
- if err != nil {
- return err
- }
-
- if active != nil {
- lm.SetActiveV3(active, features...)
- } else {
- zap.L().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 = model.BasicPlan
- setDefaultFeatures(lm)
- err := lm.InitFeatures(lm.activeFeatures)
- if err != nil {
- zap.L().Error("Couldn't initialize features", zap.Error(err))
- return err
- }
- }
-
- return nil
-}
-
-func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) {
-
- licenses, err := lm.repo.GetLicensesV3(ctx)
- if err != nil {
- return nil, model.InternalError(err)
- }
-
- for _, l := range licenses {
- if lm.activeLicenseV3 != nil && l.Key == lm.activeLicenseV3.Key {
- l.IsCurrent = true
- }
- if l.ValidUntil == -1 {
- // for subscriptions, there is no end-date as such
- // but for showing user some validity we default one year timespan
- l.ValidUntil = l.ValidFrom + 31556926
- }
- response = append(response, l)
- }
-
- return response, nil
-}
-
-// Validator validates license after an epoch of time
-func (lm *Manager) ValidatorV3(ctx context.Context) {
- zap.L().Info("ValidatorV3 started!")
- defer close(lm.terminated)
-
- tick := time.NewTicker(validationFrequency)
- defer tick.Stop()
-
- _ = lm.ValidateV3(ctx)
- for {
- select {
- case <-lm.done:
- return
- default:
- select {
- case <-lm.done:
- return
- case <-tick.C:
- _ = lm.ValidateV3(ctx)
- }
- }
-
- }
-}
-
-func (lm *Manager) RefreshLicense(ctx context.Context) error {
- license, err := validate.ValidateLicenseV3(ctx, lm.activeLicenseV3.Key, lm.zeus)
- if err != nil {
- return err
- }
-
- err = lm.repo.UpdateLicenseV3(ctx, license)
- if err != nil {
- return err
- }
- lm.SetActiveV3(license)
-
- return nil
-}
-
-func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
- if lm.activeLicenseV3 == nil {
- return nil
- }
-
- defer func() {
- lm.mutex.Lock()
-
- lm.lastValidated = time.Now().Unix()
- if reterr != nil {
- zap.L().Error("License validation completed with error", zap.Error(reterr))
-
- atomic.AddUint64(&lm.failedAttempts, 1)
- // default to basic plan if validation fails for three consecutive times
- if atomic.LoadUint64(&lm.failedAttempts) > 3 {
- zap.L().Error("License validation completed with error for three consecutive times, defaulting to basic plan", zap.String("license_id", lm.activeLicenseV3.ID), zap.Bool("license_validation", false))
- lm.activeLicenseV3 = nil
- lm.activeFeatures = model.BasicPlan
- setDefaultFeatures(lm)
- err := lm.InitFeatures(lm.activeFeatures)
- if err != nil {
- zap.L().Error("Couldn't initialize features", zap.Error(err))
- }
- lm.done <- struct{}{}
- lm.validatorRunning = false
- }
-
- telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
- map[string]interface{}{"err": reterr.Error()}, "", true, false)
- } else {
- // reset the failed attempts counter
- atomic.StoreUint64(&lm.failedAttempts, 0)
- zap.L().Info("License validation completed with no errors")
- }
-
- lm.mutex.Unlock()
- }()
-
- err := lm.RefreshLicense(ctx)
-
- if err != nil {
- return err
- }
- return nil
-}
-
-func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (*model.LicenseV3, error) {
- license, err := validate.ValidateLicenseV3(ctx, licenseKey, lm.zeus)
- if err != nil {
- return nil, err
- }
-
- // insert the new license to the sqlite db
- modelErr := lm.repo.InsertLicenseV3(ctx, license)
- if modelErr != nil {
- zap.L().Error("failed to activate license", zap.Error(modelErr))
- return nil, modelErr
- }
-
- // license is valid, activate it
- lm.SetActiveV3(license)
- return license, nil
-}
-
-func (lm *Manager) GetActiveLicense() *model.LicenseV3 {
- return lm.activeLicenseV3
-}
-
-// CheckFeature will be internally used by backend routines
-// for feature gating
-func (lm *Manager) CheckFeature(featureKey string) error {
- feature, err := lm.repo.GetFeature(featureKey)
- if err != nil {
- return err
- }
- if feature.Active {
- return nil
- }
- return basemodel.ErrFeatureUnavailable{Key: featureKey}
-}
-
-// GetFeatureFlags returns current active features
-func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) {
- return lm.repo.GetAllFeatures()
-}
-
-func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error {
- featureStatus := make([]types.FeatureStatus, len(features))
- for i, f := range features {
- featureStatus[i] = types.FeatureStatus{
- Name: f.Name,
- Active: f.Active,
- Usage: int(f.Usage),
- UsageLimit: int(f.UsageLimit),
- Route: f.Route,
- }
- }
- return lm.repo.InitFeatures(featureStatus)
-}
-
-func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error {
- return lm.repo.UpdateFeature(types.FeatureStatus{
- Name: feature.Name,
- Active: feature.Active,
- Usage: int(feature.Usage),
- UsageLimit: int(feature.UsageLimit),
- Route: feature.Route,
- })
-}
-
-func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) {
- featureStatus, err := lm.repo.GetFeature(key)
- if err != nil {
- return basemodel.Feature{}, err
- }
- return basemodel.Feature{
- Name: featureStatus.Name,
- Active: featureStatus.Active,
- Usage: int64(featureStatus.Usage),
- UsageLimit: int64(featureStatus.UsageLimit),
- Route: featureStatus.Route,
- }, nil
-}
-
-// GetRepo return the license repo
-func (lm *Manager) GetRepo() *Repo {
- return lm.repo
-}
diff --git a/ee/query-service/main.go b/ee/query-service/main.go
deleted file mode 100644
index 22b307dc123..00000000000
--- a/ee/query-service/main.go
+++ /dev/null
@@ -1,182 +0,0 @@
-package main
-
-import (
- "context"
- "flag"
- "os"
- "time"
-
- eeuserimpl "github.com/SigNoz/signoz/ee/modules/user/impluser"
- "github.com/SigNoz/signoz/ee/query-service/app"
- "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
- "github.com/SigNoz/signoz/ee/zeus"
- "github.com/SigNoz/signoz/ee/zeus/httpzeus"
- "github.com/SigNoz/signoz/pkg/config"
- "github.com/SigNoz/signoz/pkg/config/envprovider"
- "github.com/SigNoz/signoz/pkg/config/fileprovider"
- "github.com/SigNoz/signoz/pkg/modules/user"
- baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
- "github.com/SigNoz/signoz/pkg/signoz"
- "github.com/SigNoz/signoz/pkg/sqlstore"
- "github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
- "github.com/SigNoz/signoz/pkg/types/authtypes"
- "github.com/SigNoz/signoz/pkg/version"
-
- "go.uber.org/zap"
- "go.uber.org/zap/zapcore"
-)
-
-// Deprecated: Please use the logger from pkg/instrumentation.
-func initZapLog() *zap.Logger {
- config := zap.NewProductionConfig()
- config.EncoderConfig.TimeKey = "timestamp"
- config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
- logger, _ := config.Build()
- return logger
-}
-
-func main() {
- var promConfigPath, skipTopLvlOpsPath string
-
- // disables rule execution but allows change to the rule definition
- var disableRules bool
-
- // the url used to build link in the alert messages in slack and other systems
- var ruleRepoURL string
- var cluster string
-
- var useLogsNewSchema bool
- var useTraceNewSchema bool
- var cacheConfigPath, fluxInterval, fluxIntervalForTraceDetail string
- var preferSpanMetrics bool
-
- var maxIdleConns int
- var maxOpenConns int
- var dialTimeout time.Duration
- var gatewayUrl string
- var useLicensesV3 bool
-
- // Deprecated
- flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
- // Deprecated
- flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
- // Deprecated
- flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
- // Deprecated
- flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
- // Deprecated
- flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
- flag.BoolVar(&preferSpanMetrics, "prefer-span-metrics", false, "(prefer span metrics for service level metrics)")
- // Deprecated
- flag.IntVar(&maxIdleConns, "max-idle-conns", 50, "(number of connections to maintain in the pool.)")
- // Deprecated
- flag.IntVar(&maxOpenConns, "max-open-conns", 100, "(max connections for use at any time.)")
- // Deprecated
- flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)")
- // Deprecated
- flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
- // Deprecated
- flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
- flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)")
- flag.StringVar(&fluxIntervalForTraceDetail, "flux-interval-trace-detail", "2m", "(the interval to exclude data from being cached to avoid incorrect cache for trace data in motion)")
- flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
- flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
- // Deprecated
- flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
- flag.Parse()
-
- loggerMgr := initZapLog()
- zap.ReplaceGlobals(loggerMgr)
- defer loggerMgr.Sync() // flushes buffer, if any
-
- config, err := signoz.NewConfig(context.Background(), config.ResolverConfig{
- Uris: []string{"env:"},
- ProviderFactories: []config.ProviderFactory{
- envprovider.NewFactory(),
- fileprovider.NewFactory(),
- },
- }, signoz.DeprecatedFlags{
- MaxIdleConns: maxIdleConns,
- MaxOpenConns: maxOpenConns,
- DialTimeout: dialTimeout,
- Config: promConfigPath,
- })
- if err != nil {
- zap.L().Fatal("Failed to create config", zap.Error(err))
- }
-
- version.Info.PrettyPrint(config.Version)
-
- sqlStoreFactories := signoz.NewSQLStoreProviderFactories()
- if err := sqlStoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
- zap.L().Fatal("Failed to add postgressqlstore factory", zap.Error(err))
- }
-
- signoz, err := signoz.New(
- context.Background(),
- config,
- zeus.Config(),
- httpzeus.NewProviderFactory(),
- signoz.NewCacheProviderFactories(),
- signoz.NewWebProviderFactories(),
- sqlStoreFactories,
- signoz.NewTelemetryStoreProviderFactories(),
- func(sqlstore sqlstore.SQLStore) user.Module {
- return eeuserimpl.NewModule(eeuserimpl.NewStore(sqlstore))
- },
- func(userModule user.Module) user.Handler {
- return eeuserimpl.NewHandler(userModule)
- },
- )
- if err != nil {
- zap.L().Fatal("Failed to create signoz", zap.Error(err))
- }
-
- jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
-
- if len(jwtSecret) == 0 {
- zap.L().Warn("No JWT secret key is specified.")
- } else {
- zap.L().Info("JWT secret key set successfully.")
- }
-
- jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
-
- serverOptions := &app.ServerOptions{
- Config: config,
- SigNoz: signoz,
- HTTPHostPort: baseconst.HTTPHostPort,
- PreferSpanMetrics: preferSpanMetrics,
- PrivateHostPort: baseconst.PrivateHostPort,
- FluxInterval: fluxInterval,
- FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
- Cluster: cluster,
- GatewayUrl: gatewayUrl,
- Jwt: jwt,
- }
-
- server, err := app.NewServer(serverOptions)
- if err != nil {
- zap.L().Fatal("Failed to create server", zap.Error(err))
- }
-
- if err := server.Start(context.Background()); err != nil {
- zap.L().Fatal("Could not start server", zap.Error(err))
- }
-
- signoz.Start(context.Background())
-
- if err := signoz.Wait(context.Background()); err != nil {
- zap.L().Fatal("Failed to start signoz", zap.Error(err))
- }
-
- err = server.Stop()
- if err != nil {
- zap.L().Fatal("Failed to stop server", zap.Error(err))
- }
-
- err = signoz.Stop(context.Background())
- if err != nil {
- zap.L().Fatal("Failed to stop signoz", zap.Error(err))
- }
-}
diff --git a/ee/query-service/model/errors.go b/ee/query-service/model/errors.go
index cbbb0e4cc30..d3725e83299 100644
--- a/ee/query-service/model/errors.go
+++ b/ee/query-service/model/errors.go
@@ -1,7 +1,7 @@
package model
import (
- "fmt"
+ "errors"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
)
@@ -57,7 +57,7 @@ func Unauthorized(err error) *ApiError {
func BadRequestStr(s string) *ApiError {
return &ApiError{
Typ: basemodel.ErrorBadData,
- Err: fmt.Errorf(s),
+ Err: errors.New(s),
}
}
@@ -73,7 +73,7 @@ func InternalError(err error) *ApiError {
func InternalErrorStr(s string) *ApiError {
return &ApiError{
Typ: basemodel.ErrorInternal,
- Err: fmt.Errorf(s),
+ Err: errors.New(s),
}
}
diff --git a/ee/query-service/model/license.go b/ee/query-service/model/license.go
deleted file mode 100644
index 513d080891c..00000000000
--- a/ee/query-service/model/license.go
+++ /dev/null
@@ -1,244 +0,0 @@
-package model
-
-import (
- "encoding/json"
- "fmt"
- "reflect"
- "time"
-
- basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
- "github.com/pkg/errors"
-)
-
-type License struct {
- Key string `json:"key" db:"key"`
- ActivationId string `json:"activationId" db:"activationId"`
- CreatedAt time.Time `db:"created_at"`
-
- // PlanDetails contains the encrypted plan info
- PlanDetails string `json:"planDetails" db:"planDetails"`
-
- // stores parsed license details
- LicensePlan
-
- FeatureSet basemodel.FeatureSet
-
- // populated in case license has any errors
- ValidationMessage string `db:"validationMessage"`
-
- // used only for sending details to front-end
- IsCurrent bool `json:"isCurrent"`
-}
-
-func (l *License) MarshalJSON() ([]byte, error) {
-
- return json.Marshal(&struct {
- Key string `json:"key" db:"key"`
- ActivationId string `json:"activationId" db:"activationId"`
- ValidationMessage string `db:"validationMessage"`
- IsCurrent bool `json:"isCurrent"`
- PlanKey string `json:"planKey"`
- ValidFrom time.Time `json:"ValidFrom"`
- ValidUntil time.Time `json:"ValidUntil"`
- Status string `json:"status"`
- }{
- Key: l.Key,
- ActivationId: l.ActivationId,
- IsCurrent: l.IsCurrent,
- PlanKey: l.PlanKey,
- ValidFrom: time.Unix(l.ValidFrom, 0),
- ValidUntil: time.Unix(l.ValidUntil, 0),
- Status: l.Status,
- ValidationMessage: l.ValidationMessage,
- })
-}
-
-type LicensePlan struct {
- PlanKey string `json:"planKey"`
- ValidFrom int64 `json:"validFrom"`
- ValidUntil int64 `json:"validUntil"`
- Status string `json:"status"`
-}
-
-type Licenses struct {
- TrialStart int64 `json:"trialStart"`
- TrialEnd int64 `json:"trialEnd"`
- OnTrial bool `json:"onTrial"`
- WorkSpaceBlock bool `json:"workSpaceBlock"`
- TrialConvertedToSubscription bool `json:"trialConvertedToSubscription"`
- GracePeriodEnd int64 `json:"gracePeriodEnd"`
- Licenses []License `json:"licenses"`
-}
-
-type SubscriptionServerResp struct {
- Status string `json:"status"`
- Data Licenses `json:"data"`
-}
-
-type Plan struct {
- Name string `json:"name"`
-}
-
-type LicenseDB struct {
- ID string `json:"id"`
- Key string `json:"key"`
- Data string `json:"data"`
-}
-type LicenseV3 struct {
- ID string
- Key string
- Data map[string]interface{}
- PlanName string
- Features basemodel.FeatureSet
- Status string
- IsCurrent bool
- ValidFrom int64
- ValidUntil int64
-}
-
-func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) {
- var zeroValue T
- if val, ok := data[key]; ok {
- if value, ok := val.(T); ok {
- return value, nil
- }
- return zeroValue, fmt.Errorf("%s key is not a valid %s", key, reflect.TypeOf(zeroValue))
- }
- return zeroValue, fmt.Errorf("%s key is missing", key)
-}
-
-func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
- var features basemodel.FeatureSet
-
- // extract id from data
- licenseID, err := extractKeyFromMapStringInterface[string](data, "id")
- if err != nil {
- return nil, err
- }
- delete(data, "id")
-
- // extract key from data
- licenseKey, err := extractKeyFromMapStringInterface[string](data, "key")
- if err != nil {
- return nil, err
- }
- delete(data, "key")
-
- // extract status from data
- status, err := extractKeyFromMapStringInterface[string](data, "status")
- if err != nil {
- return nil, err
- }
-
- planMap, err := extractKeyFromMapStringInterface[map[string]any](data, "plan")
- if err != nil {
- return nil, err
- }
-
- planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
- if err != nil {
- return nil, err
- }
- // if license status is invalid then default it to basic
- if status == LicenseStatusInvalid {
- planName = PlanNameBasic
- }
-
- featuresFromZeus := basemodel.FeatureSet{}
- if _features, ok := data["features"]; ok {
- featuresData, err := json.Marshal(_features)
- if err != nil {
- return nil, errors.Wrap(err, "failed to marshal features data")
- }
-
- if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
- return nil, errors.Wrap(err, "failed to unmarshal features data")
- }
- }
-
- switch planName {
- case PlanNameEnterprise:
- features = append(features, EnterprisePlan...)
- case PlanNameBasic:
- features = append(features, BasicPlan...)
- default:
- features = append(features, BasicPlan...)
- }
-
- if len(featuresFromZeus) > 0 {
- for _, feature := range featuresFromZeus {
- exists := false
- for i, existingFeature := range features {
- if existingFeature.Name == feature.Name {
- features[i] = feature // Replace existing feature
- exists = true
- break
- }
- }
- if !exists {
- features = append(features, feature) // Append if it doesn't exist
- }
- }
- }
- data["features"] = features
-
- _validFrom, err := extractKeyFromMapStringInterface[float64](data, "valid_from")
- if err != nil {
- _validFrom = 0
- }
- validFrom := int64(_validFrom)
-
- _validUntil, err := extractKeyFromMapStringInterface[float64](data, "valid_until")
- if err != nil {
- _validUntil = 0
- }
- validUntil := int64(_validUntil)
-
- return &LicenseV3{
- ID: licenseID,
- Key: licenseKey,
- Data: data,
- PlanName: planName,
- Features: features,
- ValidFrom: validFrom,
- ValidUntil: validUntil,
- Status: status,
- }, nil
-
-}
-
-func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}) (*LicenseV3, error) {
- licenseDataWithIdAndKey := data
- licenseDataWithIdAndKey["id"] = id
- licenseDataWithIdAndKey["key"] = key
- return NewLicenseV3(licenseDataWithIdAndKey)
-}
-
-func ConvertLicenseV3ToLicenseV2(l *LicenseV3) *License {
- planKeyFromPlanName, ok := MapOldPlanKeyToNewPlanName[l.PlanName]
- if !ok {
- planKeyFromPlanName = Basic
- }
- return &License{
- Key: l.Key,
- ActivationId: "",
- PlanDetails: "",
- FeatureSet: l.Features,
- ValidationMessage: "",
- IsCurrent: l.IsCurrent,
- LicensePlan: LicensePlan{
- PlanKey: planKeyFromPlanName,
- ValidFrom: l.ValidFrom,
- ValidUntil: l.ValidUntil,
- Status: l.Status},
- }
-
-}
-
-type CheckoutRequest struct {
- SuccessURL string `json:"url"`
-}
-
-type PortalRequest struct {
- SuccessURL string `json:"url"`
-}
diff --git a/ee/query-service/model/license_test.go b/ee/query-service/model/license_test.go
deleted file mode 100644
index 710541eea3f..00000000000
--- a/ee/query-service/model/license_test.go
+++ /dev/null
@@ -1,170 +0,0 @@
-package model
-
-import (
- "encoding/json"
- "testing"
-
- "github.com/SigNoz/signoz/pkg/query-service/model"
- "github.com/pkg/errors"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestNewLicenseV3(t *testing.T) {
- testCases := []struct {
- name string
- data []byte
- pass bool
- expected *LicenseV3
- error error
- }{
- {
- name: "Error for missing license id",
- data: []byte(`{}`),
- pass: false,
- error: errors.New("id key is missing"),
- },
- {
- name: "Error for license id not being a valid string",
- data: []byte(`{"id": 10}`),
- pass: false,
- error: errors.New("id key is not a valid string"),
- },
- {
- name: "Error for missing license key",
- data: []byte(`{"id":"does-not-matter"}`),
- pass: false,
- error: errors.New("key key is missing"),
- },
- {
- name: "Error for invalid string license key",
- data: []byte(`{"id":"does-not-matter","key":10}`),
- pass: false,
- error: errors.New("key key is not a valid string"),
- },
- {
- name: "Error for missing license status",
- data: []byte(`{"id":"does-not-matter", "key": "does-not-matter","category":"FREE"}`),
- pass: false,
- error: errors.New("status key is missing"),
- },
- {
- name: "Error for invalid string license status",
- data: []byte(`{"id":"does-not-matter","key": "does-not-matter", "category":"FREE", "status":10}`),
- pass: false,
- error: errors.New("status key is not a valid string"),
- },
- {
- name: "Error for missing license plan",
- data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE"}`),
- pass: false,
- error: errors.New("plan key is missing"),
- },
- {
- name: "Error for invalid json license plan",
- data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":10}`),
- pass: false,
- error: errors.New("plan key is not a valid map[string]interface {}"),
- },
- {
- name: "Error for invalid license plan",
- data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{}}`),
- pass: false,
- error: errors.New("name key is missing"),
- },
- {
- name: "Parse the entire license properly",
- data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`),
- pass: true,
- expected: &LicenseV3{
- ID: "does-not-matter",
- Key: "does-not-matter-key",
- Data: map[string]interface{}{
- "plan": map[string]interface{}{
- "name": "ENTERPRISE",
- },
- "category": "FREE",
- "status": "ACTIVE",
- "valid_from": float64(1730899309),
- "valid_until": float64(-1),
- },
- PlanName: PlanNameEnterprise,
- ValidFrom: 1730899309,
- ValidUntil: -1,
- Status: "ACTIVE",
- IsCurrent: false,
- Features: model.FeatureSet{},
- },
- },
- {
- name: "Fallback to basic plan if license status is invalid",
- data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`),
- pass: true,
- expected: &LicenseV3{
- ID: "does-not-matter",
- Key: "does-not-matter-key",
- Data: map[string]interface{}{
- "plan": map[string]interface{}{
- "name": "ENTERPRISE",
- },
- "category": "FREE",
- "status": "INVALID",
- "valid_from": float64(1730899309),
- "valid_until": float64(-1),
- },
- PlanName: PlanNameBasic,
- ValidFrom: 1730899309,
- ValidUntil: -1,
- Status: "INVALID",
- IsCurrent: false,
- Features: model.FeatureSet{},
- },
- },
- {
- name: "fallback states for validFrom and validUntil",
- data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from":1234.456,"valid_until":5678.567}`),
- pass: true,
- expected: &LicenseV3{
- ID: "does-not-matter",
- Key: "does-not-matter-key",
- Data: map[string]interface{}{
- "plan": map[string]interface{}{
- "name": "ENTERPRISE",
- },
- "valid_from": 1234.456,
- "valid_until": 5678.567,
- "category": "FREE",
- "status": "ACTIVE",
- },
- PlanName: PlanNameEnterprise,
- ValidFrom: 1234,
- ValidUntil: 5678,
- Status: "ACTIVE",
- IsCurrent: false,
- Features: model.FeatureSet{},
- },
- },
- }
-
- for _, tc := range testCases {
- var licensePayload map[string]interface{}
- err := json.Unmarshal(tc.data, &licensePayload)
- require.NoError(t, err)
- license, err := NewLicenseV3(licensePayload)
- if license != nil {
- license.Features = make(model.FeatureSet, 0)
- delete(license.Data, "features")
- }
-
- if tc.pass {
- require.NoError(t, err)
- require.NotNil(t, license)
- assert.Equal(t, tc.expected, license)
- } else {
- require.Error(t, err)
- assert.EqualError(t, err, tc.error.Error())
- require.Nil(t, license)
- }
-
- }
-}
diff --git a/ee/query-service/model/plans.go b/ee/query-service/model/plans.go
deleted file mode 100644
index 2de2e7ccb87..00000000000
--- a/ee/query-service/model/plans.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package model
-
-import (
- basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
-)
-
-const SSO = "SSO"
-const Basic = "BASIC_PLAN"
-const Enterprise = "ENTERPRISE_PLAN"
-
-var (
- PlanNameEnterprise = "ENTERPRISE"
- PlanNameBasic = "BASIC"
-)
-
-var (
- MapOldPlanKeyToNewPlanName map[string]string = map[string]string{PlanNameBasic: Basic, PlanNameEnterprise: Enterprise}
-)
-
-var (
- LicenseStatusInvalid = "INVALID"
-)
-
-const Onboarding = "ONBOARDING"
-const ChatSupport = "CHAT_SUPPORT"
-const Gateway = "GATEWAY"
-const PremiumSupport = "PREMIUM_SUPPORT"
-
-var BasicPlan = basemodel.FeatureSet{
- basemodel.Feature{
- Name: SSO,
- Active: false,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: basemodel.UseSpanMetrics,
- Active: false,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: Gateway,
- Active: false,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: PremiumSupport,
- Active: false,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: basemodel.AnomalyDetection,
- Active: false,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: basemodel.TraceFunnels,
- Active: false,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
-}
-
-var EnterprisePlan = basemodel.FeatureSet{
- basemodel.Feature{
- Name: SSO,
- Active: true,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: basemodel.UseSpanMetrics,
- Active: false,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: Onboarding,
- Active: true,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: ChatSupport,
- Active: true,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: Gateway,
- Active: true,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: PremiumSupport,
- Active: true,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: basemodel.AnomalyDetection,
- Active: true,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
- basemodel.Feature{
- Name: basemodel.TraceFunnels,
- Active: false,
- Usage: 0,
- UsageLimit: -1,
- Route: "",
- },
-}
diff --git a/ee/query-service/rules/anomaly.go b/ee/query-service/rules/anomaly.go
index 78801f1f1c5..af988b5d677 100644
--- a/ee/query-service/rules/anomaly.go
+++ b/ee/query-service/rules/anomaly.go
@@ -4,17 +4,17 @@ import (
"context"
"encoding/json"
"fmt"
+ "log/slog"
"math"
"strings"
"sync"
"time"
- "go.uber.org/zap"
-
"github.com/SigNoz/signoz/ee/query-service/anomaly"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/query-service/model"
+ "github.com/SigNoz/signoz/pkg/transition"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -30,7 +30,11 @@ import (
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
- yaml "gopkg.in/yaml.v2"
+ querierV5 "github.com/SigNoz/signoz/pkg/querier"
+
+ anomalyV2 "github.com/SigNoz/signoz/ee/anomaly"
+
+ qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
const (
@@ -47,7 +51,14 @@ type AnomalyRule struct {
// querierV2 is used for alerts created after the introduction of new metrics query builder
querierV2 interfaces.Querier
- provider anomaly.Provider
+ // querierV5 is used for alerts migrated after the introduction of new query builder
+ querierV5 querierV5.Querier
+
+ provider anomaly.Provider
+ providerV2 anomalyV2.Provider
+
+ version string
+ logger *slog.Logger
seasonality anomaly.Seasonality
}
@@ -57,11 +68,15 @@ func NewAnomalyRule(
orgID valuer.UUID,
p *ruletypes.PostableRule,
reader interfaces.Reader,
+ querierV5 querierV5.Querier,
+ logger *slog.Logger,
cache cache.Cache,
opts ...baserules.RuleOption,
) (*AnomalyRule, error) {
- zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts))
+ logger.Info("creating new AnomalyRule", "rule_id", id)
+
+ opts = append(opts, baserules.WithLogger(logger))
if p.RuleCondition.CompareOp == ruletypes.ValueIsBelow {
target := -1 * *p.RuleCondition.Target
@@ -88,7 +103,7 @@ func NewAnomalyRule(
t.seasonality = anomaly.SeasonalityDaily
}
- zap.L().Info("using seasonality", zap.String("seasonality", t.seasonality.String()))
+ logger.Info("using seasonality", "seasonality", t.seasonality.String())
querierOptsV2 := querierV2.QuerierOptions{
Reader: reader,
@@ -117,6 +132,27 @@ func NewAnomalyRule(
anomaly.WithReader[*anomaly.WeeklyProvider](reader),
)
}
+
+ if t.seasonality == anomaly.SeasonalityHourly {
+ t.providerV2 = anomalyV2.NewHourlyProvider(
+ anomalyV2.WithQuerier[*anomalyV2.HourlyProvider](querierV5),
+ anomalyV2.WithLogger[*anomalyV2.HourlyProvider](logger),
+ )
+ } else if t.seasonality == anomaly.SeasonalityDaily {
+ t.providerV2 = anomalyV2.NewDailyProvider(
+ anomalyV2.WithQuerier[*anomalyV2.DailyProvider](querierV5),
+ anomalyV2.WithLogger[*anomalyV2.DailyProvider](logger),
+ )
+ } else if t.seasonality == anomaly.SeasonalityWeekly {
+ t.providerV2 = anomalyV2.NewWeeklyProvider(
+ anomalyV2.WithQuerier[*anomalyV2.WeeklyProvider](querierV5),
+ anomalyV2.WithLogger[*anomalyV2.WeeklyProvider](logger),
+ )
+ }
+
+ t.querierV5 = querierV5
+ t.version = p.Version
+ t.logger = logger
return &t, nil
}
@@ -124,20 +160,15 @@ func (r *AnomalyRule) Type() ruletypes.RuleType {
return RuleTypeAnomaly
}
-func (r *AnomalyRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, error) {
+func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) (*v3.QueryRangeParamsV3, error) {
- zap.L().Info("prepareQueryRange", zap.Int64("ts", ts.UnixMilli()), zap.Int64("evalWindow", r.EvalWindow().Milliseconds()), zap.Int64("evalDelay", r.EvalDelay().Milliseconds()))
+ r.logger.InfoContext(
+ ctx, "prepare query range request v4", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds(),
+ )
- start := ts.Add(-time.Duration(r.EvalWindow())).UnixMilli()
- end := ts.UnixMilli()
-
- if r.EvalDelay() > 0 {
- start = start - int64(r.EvalDelay().Milliseconds())
- end = end - int64(r.EvalDelay().Milliseconds())
- }
- // round to minute otherwise we could potentially miss data
- start = start - (start % (60 * 1000))
- end = end - (end % (60 * 1000))
+ st, en := r.Timestamps(ts)
+ start := st.UnixMilli()
+ end := en.UnixMilli()
compositeQuery := r.Condition().CompositeQuery
@@ -156,13 +187,34 @@ func (r *AnomalyRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, e
}, nil
}
+func (r *AnomalyRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) (*qbtypes.QueryRangeRequest, error) {
+
+ r.logger.InfoContext(ctx, "prepare query range request v5", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds())
+
+ startTs, endTs := r.Timestamps(ts)
+ start, end := startTs.UnixMilli(), endTs.UnixMilli()
+
+ req := &qbtypes.QueryRangeRequest{
+ Start: uint64(start),
+ End: uint64(end),
+ RequestType: qbtypes.RequestTypeTimeSeries,
+ CompositeQuery: qbtypes.CompositeQuery{
+ Queries: make([]qbtypes.QueryEnvelope, 0),
+ },
+ NoCache: true,
+ }
+ req.CompositeQuery.Queries = make([]qbtypes.QueryEnvelope, len(r.Condition().CompositeQuery.Queries))
+ copy(req.CompositeQuery.Queries, r.Condition().CompositeQuery.Queries)
+ return req, nil
+}
+
func (r *AnomalyRule) GetSelectedQuery() string {
return r.Condition().GetSelectedQueryName()
}
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
- params, err := r.prepareQueryRange(ts)
+ params, err := r.prepareQueryRange(ctx, ts)
if err != nil {
return nil, err
}
@@ -190,13 +242,70 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
var resultVector ruletypes.Vector
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
- zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON)))
+ r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
for _, series := range queryResult.AnomalyScores {
- smpl, shouldAlert := r.ShouldAlert(*series)
- if shouldAlert {
- resultVector = append(resultVector, smpl)
+ if r.Condition() != nil && r.Condition().RequireMinPoints {
+ if len(series.Points) < r.Condition().RequiredNumPoints {
+ r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
+ continue
+ }
}
+ results, err := r.Threshold.ShouldAlert(*series, r.Unit())
+ if err != nil {
+ return nil, err
+ }
+ resultVector = append(resultVector, results...)
+ }
+ return resultVector, nil
+}
+
+func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
+
+ params, err := r.prepareQueryRangeV5(ctx, ts)
+ if err != nil {
+ return nil, err
+ }
+
+ anomalies, err := r.providerV2.GetAnomalies(ctx, orgID, &anomalyV2.AnomaliesRequest{
+ Params: *params,
+ Seasonality: anomalyV2.Seasonality{String: valuer.NewString(r.seasonality.String())},
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ var qbResult *qbtypes.TimeSeriesData
+ for _, result := range anomalies.Results {
+ if result.QueryName == r.GetSelectedQuery() {
+ qbResult = result
+ break
+ }
+ }
+
+ if qbResult == nil {
+ r.logger.WarnContext(ctx, "nil qb result", "ts", ts.UnixMilli())
+ }
+
+ queryResult := transition.ConvertV5TimeSeriesDataToV4Result(qbResult)
+
+ var resultVector ruletypes.Vector
+
+ scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
+ r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
+
+ for _, series := range queryResult.AnomalyScores {
+ if r.Condition().RequireMinPoints {
+ if len(series.Points) < r.Condition().RequiredNumPoints {
+ r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
+ continue
+ }
+ }
+ results, err := r.Threshold.ShouldAlert(*series, r.Unit())
+ if err != nil {
+ return nil, err
+ }
+ resultVector = append(resultVector, results...)
}
return resultVector, nil
}
@@ -206,8 +315,17 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
prevState := r.State()
valueFormatter := formatter.FromUnit(r.Unit())
- res, err := r.buildAndRunQuery(ctx, r.OrgID(), ts)
+ var res ruletypes.Vector
+ var err error
+
+ if r.version == "v5" {
+ r.logger.InfoContext(ctx, "running v5 query")
+ res, err = r.buildAndRunQueryV5(ctx, r.OrgID(), ts)
+ } else {
+ r.logger.InfoContext(ctx, "running v4 query")
+ res, err = r.buildAndRunQuery(ctx, r.OrgID(), ts)
+ }
if err != nil {
return nil, err
}
@@ -218,15 +336,20 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
resultFPs := map[uint64]struct{}{}
var alerts = make(map[uint64]*ruletypes.Alert, len(res))
+ ruleReceivers := r.Threshold.GetRuleReceivers()
+ ruleReceiverMap := make(map[string][]string)
+ for _, value := range ruleReceivers {
+ ruleReceiverMap[value.Name] = value.Channels
+ }
+
for _, smpl := range res {
l := make(map[string]string, len(smpl.Metric))
for _, lbl := range smpl.Metric {
l[lbl.Name] = lbl.Value
}
-
value := valueFormatter.Format(smpl.V, r.Unit())
- threshold := valueFormatter.Format(r.TargetVal(), r.Unit())
- zap.L().Debug("Alert template data for rule", zap.String("name", r.Name()), zap.String("formatter", valueFormatter.Name()), zap.String("value", value), zap.String("threshold", threshold))
+ threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
+ r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
// Inject some convenience variables that are easier to remember for users
@@ -247,7 +370,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
result, err := tmpl.Expand()
if err != nil {
result = fmt.Sprintf("", err)
- zap.L().Error("Expanding alert template failed", zap.Error(err), zap.Any("data", tmplData))
+ r.logger.ErrorContext(ctx, "Expanding alert template failed", "error", err, "data", tmplData, "rule_name", r.Name())
}
return result
}
@@ -269,6 +392,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
}
if smpl.IsMissing {
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())
+ lb.Set(labels.NoDataLabel, "true")
}
lbs := lb.Labels()
@@ -276,7 +400,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
resultFPs[h] = struct{}{}
if _, ok := alerts[h]; ok {
- zap.L().Error("the alert query returns duplicate records", zap.String("ruleid", r.ID()), zap.Any("alert", alerts[h]))
+ r.logger.ErrorContext(ctx, "the alert query returns duplicate records", "rule_id", r.ID(), "alert", alerts[h])
err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
return nil, err
}
@@ -289,13 +413,12 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
State: model.StatePending,
Value: smpl.V,
GeneratorURL: r.GeneratorURL(),
- Receivers: r.PreferredChannels(),
+ Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
Missing: smpl.IsMissing,
}
}
- zap.L().Info("number of alerts found", zap.String("name", r.Name()), zap.Int("count", len(alerts)))
-
+ r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
// alerts[h] is ready, add or update active list now
for h, a := range alerts {
// Check whether we already have alerting state for the identifying label set.
@@ -304,7 +427,9 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
alert.Value = a.Value
alert.Annotations = a.Annotations
- alert.Receivers = r.PreferredChannels()
+ if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
+ alert.Receivers = ruleReceiverMap[v]
+ }
continue
}
@@ -317,7 +442,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
for fp, a := range r.Active {
labelsJSON, err := json.Marshal(a.QueryResultLables)
if err != nil {
- zap.L().Error("error marshaling labels", zap.Error(err), zap.Any("labels", a.Labels))
+ r.logger.ErrorContext(ctx, "error marshaling labels", "error", err, "labels", a.Labels)
}
if _, ok := resultFPs[fp]; !ok {
// If the alert was previously firing, keep it around for a given
@@ -387,7 +512,7 @@ func (r *AnomalyRule) String() string {
PreferredChannels: r.PreferredChannels(),
}
- byt, err := yaml.Marshal(ar)
+ byt, err := json.Marshal(ar)
if err != nil {
return fmt.Sprintf("error marshaling alerting rule: %s", err.Error())
}
diff --git a/ee/query-service/rules/manager.go b/ee/query-service/rules/manager.go
index 48550558dc0..31009b3c309 100644
--- a/ee/query-service/rules/manager.go
+++ b/ee/query-service/rules/manager.go
@@ -3,8 +3,10 @@ package rules
import (
"context"
"fmt"
+
"time"
+ "github.com/SigNoz/signoz/pkg/errors"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
@@ -20,6 +22,10 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
var task baserules.Task
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
+ evaluation, err := opts.Rule.Evaluation.GetEvaluation()
+ if err != nil {
+ return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
+ }
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := baserules.NewThresholdRule(
@@ -27,6 +33,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.OrgID,
opts.Rule,
opts.Reader,
+ opts.Querier,
+ opts.SLogger,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
)
@@ -38,7 +46,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, tr)
// create ch rule task for evalution
- task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
+ task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
@@ -47,7 +55,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
ruleId,
opts.OrgID,
opts.Rule,
- opts.Logger,
+ opts.SLogger,
opts.Reader,
opts.ManagerOpts.Prometheus,
baserules.WithSQLStore(opts.SQLStore),
@@ -60,7 +68,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, pr)
// create promql rule task for evalution
- task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
+ task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
// create anomaly rule
@@ -69,6 +77,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.OrgID,
opts.Rule,
opts.Reader,
+ opts.Querier,
+ opts.SLogger,
opts.Cache,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
@@ -80,7 +90,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, ar)
// create anomaly rule task for evalution
- task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
+ task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(evaluation.GetFrequency()), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else {
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
@@ -116,7 +126,6 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
// add special labels for test alerts
- parsedRule.Annotations[labels.AlertSummaryLabel] = fmt.Sprintf("The rule threshold is set to %.4f, and the observed metric value is {{$value}}.", *parsedRule.RuleCondition.Target)
parsedRule.Labels[labels.RuleSourceLabel] = ""
parsedRule.Labels[labels.AlertRuleIdLabel] = ""
@@ -126,6 +135,8 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
opts.OrgID,
parsedRule,
opts.Reader,
+ opts.Querier,
+ opts.SLogger,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),
@@ -143,7 +154,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
alertname,
opts.OrgID,
parsedRule,
- opts.Logger,
+ opts.SLogger,
opts.Reader,
opts.ManagerOpts.Prometheus,
baserules.WithSendAlways(),
@@ -162,6 +173,8 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
opts.OrgID,
parsedRule,
opts.Reader,
+ opts.Querier,
+ opts.SLogger,
opts.Cache,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
diff --git a/ee/query-service/usage/manager.go b/ee/query-service/usage/manager.go
index e42946cf9c0..ad8c8ec4baa 100644
--- a/ee/query-service/usage/manager.go
+++ b/ee/query-service/usage/manager.go
@@ -14,9 +14,9 @@ import (
"go.uber.org/zap"
- "github.com/SigNoz/signoz/ee/query-service/dao"
- "github.com/SigNoz/signoz/ee/query-service/license"
"github.com/SigNoz/signoz/ee/query-service/model"
+ "github.com/SigNoz/signoz/pkg/licensing"
+ "github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/query-service/utils/encryption"
"github.com/SigNoz/signoz/pkg/zeus"
)
@@ -35,64 +35,68 @@ var (
type Manager struct {
clickhouseConn clickhouse.Conn
- licenseRepo *license.Repo
+ licenseService licensing.Licensing
scheduler *gocron.Scheduler
- modelDao dao.ModelDao
-
zeus zeus.Zeus
+
+ orgGetter organization.Getter
}
-func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn, zeus zeus.Zeus) (*Manager, error) {
+func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, orgGetter organization.Getter) (*Manager, error) {
m := &Manager{
clickhouseConn: clickhouseConn,
- licenseRepo: licenseRepo,
+ licenseService: licenseService,
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
- modelDao: modelDao,
zeus: zeus,
+ orgGetter: orgGetter,
}
return m, nil
}
// start loads collects and exports any exported snapshot and starts the exporter
-func (lm *Manager) Start() error {
+func (lm *Manager) Start(ctx context.Context) error {
// compares the locker and stateUnlocked if both are same lock is applied else returns error
if !atomic.CompareAndSwapUint32(&locker, stateUnlocked, stateLocked) {
return fmt.Errorf("usage exporter is locked")
}
- _, err := lm.scheduler.Do(func() { lm.UploadUsage() })
+ // upload usage once when starting the service
+
+ _, err := lm.scheduler.Do(func() { lm.UploadUsage(ctx) })
if err != nil {
return err
}
- // upload usage once when starting the service
- lm.UploadUsage()
-
+ lm.UploadUsage(ctx)
lm.scheduler.StartAsync()
-
return nil
}
-func (lm *Manager) UploadUsage() {
- ctx := context.Background()
- // check if license is present or not
- license, err := lm.licenseRepo.GetActiveLicense(ctx)
+func (lm *Manager) UploadUsage(ctx context.Context) {
+ organizations, err := lm.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
- zap.L().Error("failed to get active license", zap.Error(err))
- return
- }
- if license == nil {
- // we will not start the usage reporting if license is not present.
- zap.L().Info("no license present, skipping usage reporting")
+ zap.L().Error("failed to get organizations", zap.Error(err))
return
}
+ for _, organization := range organizations {
+ // check if license is present or not
+ license, err := lm.licenseService.GetActive(ctx, organization.ID)
+ if err != nil {
+ zap.L().Error("failed to get active license", zap.Error(err))
+ return
+ }
+ if license == nil {
+ // we will not start the usage reporting if license is not present.
+ zap.L().Info("no license present, skipping usage reporting")
+ return
+ }
- usages := []model.UsageDB{}
+ usages := []model.UsageDB{}
- // get usage from clickhouse
- dbs := []string{"signoz_logs", "signoz_traces", "signoz_metrics"}
- query := `
+ // get usage from clickhouse
+ dbs := []string{"signoz_logs", "signoz_traces", "signoz_metrics"}
+ query := `
SELECT tenant, collector_id, exporter_id, timestamp, data
FROM %s.distributed_usage as u1
GLOBAL INNER JOIN
@@ -107,76 +111,76 @@ func (lm *Manager) UploadUsage() {
order by timestamp
`
- for _, db := range dbs {
- dbusages := []model.UsageDB{}
- err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
- if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
- zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err))
- return
+ for _, db := range dbs {
+ dbusages := []model.UsageDB{}
+ err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
+ if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
+ zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err))
+ return
+ }
+ for _, u := range dbusages {
+ u.Type = db
+ usages = append(usages, u)
+ }
}
- for _, u := range dbusages {
- u.Type = db
- usages = append(usages, u)
+
+ if len(usages) <= 0 {
+ zap.L().Info("no snapshots to upload, skipping.")
+ return
}
- }
- if len(usages) <= 0 {
- zap.L().Info("no snapshots to upload, skipping.")
- return
- }
+ zap.L().Info("uploading usage data")
+
+ usagesPayload := []model.Usage{}
+ for _, usage := range usages {
+ usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
+ if err != nil {
+ zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
+ return
+ }
+
+ usageData := model.Usage{}
+ err = json.Unmarshal(usageDataBytes, &usageData)
+ if err != nil {
+ zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
+ return
+ }
+
+ usageData.CollectorID = usage.CollectorID
+ usageData.ExporterID = usage.ExporterID
+ usageData.Type = usage.Type
+ usageData.Tenant = "default"
+ usageData.OrgName = "default"
+ usageData.TenantId = "default"
+ usagesPayload = append(usagesPayload, usageData)
+ }
- zap.L().Info("uploading usage data")
+ key, _ := uuid.Parse(license.Key)
+ payload := model.UsagePayload{
+ LicenseKey: key,
+ Usage: usagesPayload,
+ }
- usagesPayload := []model.Usage{}
- for _, usage := range usages {
- usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
- if err != nil {
- zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
+ body, errv2 := json.Marshal(payload)
+ if errv2 != nil {
+ zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2))
return
}
- usageData := model.Usage{}
- err = json.Unmarshal(usageDataBytes, &usageData)
- if err != nil {
- zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
+ errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
+ if errv2 != nil {
+ zap.L().Error("failed to upload usage: %v", zap.Error(errv2))
+ // not returning error here since it is captured in the failed count
return
}
-
- usageData.CollectorID = usage.CollectorID
- usageData.ExporterID = usage.ExporterID
- usageData.Type = usage.Type
- usageData.Tenant = "default"
- usageData.OrgName = "default"
- usageData.TenantId = "default"
- usagesPayload = append(usagesPayload, usageData)
- }
-
- key, _ := uuid.Parse(license.Key)
- payload := model.UsagePayload{
- LicenseKey: key,
- Usage: usagesPayload,
- }
-
- body, errv2 := json.Marshal(payload)
- if errv2 != nil {
- zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2))
- return
- }
-
- errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
- if errv2 != nil {
- zap.L().Error("failed to upload usage: %v", zap.Error(errv2))
- // not returning error here since it is captured in the failed count
- return
}
}
-func (lm *Manager) Stop() {
+func (lm *Manager) Stop(ctx context.Context) {
lm.scheduler.Stop()
zap.L().Info("sending usage data before shutting down")
// send usage before shutting down
- lm.UploadUsage()
-
+ lm.UploadUsage(ctx)
atomic.StoreUint32(&locker, stateUnlocked)
}
diff --git a/ee/sqlschema/postgressqlschema/formatter.go b/ee/sqlschema/postgressqlschema/formatter.go
new file mode 100644
index 00000000000..e51fd7ac951
--- /dev/null
+++ b/ee/sqlschema/postgressqlschema/formatter.go
@@ -0,0 +1,36 @@
+package postgressqlschema
+
+import (
+ "strings"
+
+ "github.com/SigNoz/signoz/pkg/sqlschema"
+)
+
+type Formatter struct {
+ sqlschema.Formatter
+}
+
+func (formatter Formatter) SQLDataTypeOf(dataType sqlschema.DataType) string {
+ if dataType == sqlschema.DataTypeTimestamp {
+ return "TIMESTAMPTZ"
+ }
+
+ return strings.ToUpper(dataType.String())
+}
+
+func (formatter Formatter) DataTypeOf(dataType string) sqlschema.DataType {
+ switch strings.ToUpper(dataType) {
+ case "TIMESTAMPTZ", "TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE", "TIMESTAMP WITH TIME ZONE":
+ return sqlschema.DataTypeTimestamp
+ case "INT8":
+ return sqlschema.DataTypeBigInt
+ case "INT2", "INT4", "SMALLINT", "INTEGER":
+ return sqlschema.DataTypeInteger
+ case "BOOL", "BOOLEAN":
+ return sqlschema.DataTypeBoolean
+ case "VARCHAR", "CHARACTER VARYING", "CHARACTER":
+ return sqlschema.DataTypeText
+ }
+
+ return formatter.Formatter.DataTypeOf(dataType)
+}
diff --git a/ee/sqlschema/postgressqlschema/provider.go b/ee/sqlschema/postgressqlschema/provider.go
new file mode 100644
index 00000000000..af06994b45b
--- /dev/null
+++ b/ee/sqlschema/postgressqlschema/provider.go
@@ -0,0 +1,285 @@
+package postgressqlschema
+
+import (
+ "context"
+
+ "github.com/SigNoz/signoz/pkg/factory"
+ "github.com/SigNoz/signoz/pkg/sqlschema"
+ "github.com/SigNoz/signoz/pkg/sqlstore"
+ "github.com/uptrace/bun"
+)
+
+type provider struct {
+ settings factory.ScopedProviderSettings
+ fmter sqlschema.SQLFormatter
+ sqlstore sqlstore.SQLStore
+ operator sqlschema.SQLOperator
+}
+
+func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config] {
+ return factory.NewProviderFactory(factory.MustNewName("postgres"), func(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config) (sqlschema.SQLSchema, error) {
+ return New(ctx, providerSettings, config, sqlstore)
+ })
+}
+
+func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config, sqlstore sqlstore.SQLStore) (sqlschema.SQLSchema, error) {
+ settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/sqlschema/postgressqlschema")
+ fmter := Formatter{Formatter: sqlschema.NewFormatter(sqlstore.BunDB().Dialect())}
+
+ return &provider{
+ sqlstore: sqlstore,
+ fmter: fmter,
+ settings: settings,
+ operator: sqlschema.NewOperator(fmter, sqlschema.OperatorSupport{
+ DropConstraint: true,
+ ColumnIfNotExistsExists: true,
+ AlterColumnSetNotNull: true,
+ }),
+ }, nil
+}
+
+func (provider *provider) Formatter() sqlschema.SQLFormatter {
+ return provider.fmter
+}
+
+func (provider *provider) Operator() sqlschema.SQLOperator {
+ return provider.operator
+}
+
+func (provider *provider) GetTable(ctx context.Context, tableName sqlschema.TableName) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
+ rows, err := provider.
+ sqlstore.
+ BunDB().
+ QueryContext(ctx, `
+SELECT
+ c.column_name,
+ c.is_nullable = 'YES',
+ c.udt_name,
+ c.column_default
+FROM
+ information_schema.columns AS c
+WHERE
+ c.table_name = ?`, string(tableName))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ defer func() {
+ if err := rows.Close(); err != nil {
+ provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
+ }
+ }()
+
+ columns := make([]*sqlschema.Column, 0)
+ for rows.Next() {
+ var (
+ name string
+ sqlDataType string
+ nullable bool
+ defaultVal *string
+ )
+ if err := rows.Scan(&name, &nullable, &sqlDataType, &defaultVal); err != nil {
+ return nil, nil, err
+ }
+
+ columnDefault := ""
+ if defaultVal != nil {
+ columnDefault = *defaultVal
+ }
+
+ columns = append(columns, &sqlschema.Column{
+ Name: sqlschema.ColumnName(name),
+ Nullable: nullable,
+ DataType: provider.fmter.DataTypeOf(sqlDataType),
+ Default: columnDefault,
+ })
+ }
+
+ constraintsRows, err := provider.
+ sqlstore.
+ BunDB().
+ QueryContext(ctx, `
+SELECT
+ c.column_name,
+ constraint_name,
+ constraint_type
+FROM
+ information_schema.table_constraints tc
+ JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_catalog, table_name, constraint_name)
+ JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
+WHERE
+ c.table_name = ?`, string(tableName))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ defer func() {
+ if err := constraintsRows.Close(); err != nil {
+ provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
+ }
+ }()
+
+ var primaryKeyConstraint *sqlschema.PrimaryKeyConstraint
+ uniqueConstraintsMap := make(map[string]*sqlschema.UniqueConstraint)
+ for constraintsRows.Next() {
+ var (
+ name string
+ constraintName string
+ constraintType string
+ )
+
+ if err := constraintsRows.Scan(&name, &constraintName, &constraintType); err != nil {
+ return nil, nil, err
+ }
+
+ if constraintType == "PRIMARY KEY" {
+ if primaryKeyConstraint == nil {
+ primaryKeyConstraint = (&sqlschema.PrimaryKeyConstraint{
+ ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
+ }).Named(constraintName).(*sqlschema.PrimaryKeyConstraint)
+ } else {
+ primaryKeyConstraint.ColumnNames = append(primaryKeyConstraint.ColumnNames, sqlschema.ColumnName(name))
+ }
+ }
+
+ if constraintType == "UNIQUE" {
+ if _, ok := uniqueConstraintsMap[constraintName]; !ok {
+ uniqueConstraintsMap[constraintName] = (&sqlschema.UniqueConstraint{
+ ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
+ }).Named(constraintName).(*sqlschema.UniqueConstraint)
+ } else {
+ uniqueConstraintsMap[constraintName].ColumnNames = append(uniqueConstraintsMap[constraintName].ColumnNames, sqlschema.ColumnName(name))
+ }
+ }
+ }
+
+ foreignKeyConstraintsRows, err := provider.
+ sqlstore.
+ BunDB().
+ QueryContext(ctx, `
+SELECT
+ tc.constraint_name,
+ kcu.table_name AS referencing_table,
+ kcu.column_name AS referencing_column,
+ ccu.table_name AS referenced_table,
+ ccu.column_name AS referenced_column
+FROM
+ information_schema.key_column_usage kcu
+ JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
+ JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
+WHERE
+ tc.constraint_type = ?
+ AND kcu.table_name = ?`, "FOREIGN KEY", string(tableName))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ defer func() {
+ if err := foreignKeyConstraintsRows.Close(); err != nil {
+ provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
+ }
+ }()
+
+ foreignKeyConstraints := make([]*sqlschema.ForeignKeyConstraint, 0)
+ for foreignKeyConstraintsRows.Next() {
+ var (
+ constraintName string
+ referencingTable string
+ referencingColumn string
+ referencedTable string
+ referencedColumn string
+ )
+
+ if err := foreignKeyConstraintsRows.Scan(&constraintName, &referencingTable, &referencingColumn, &referencedTable, &referencedColumn); err != nil {
+ return nil, nil, err
+ }
+
+ foreignKeyConstraints = append(foreignKeyConstraints, (&sqlschema.ForeignKeyConstraint{
+ ReferencingColumnName: sqlschema.ColumnName(referencingColumn),
+ ReferencedTableName: sqlschema.TableName(referencedTable),
+ ReferencedColumnName: sqlschema.ColumnName(referencedColumn),
+ }).Named(constraintName).(*sqlschema.ForeignKeyConstraint))
+ }
+
+ uniqueConstraints := make([]*sqlschema.UniqueConstraint, 0)
+ for _, uniqueConstraint := range uniqueConstraintsMap {
+ uniqueConstraints = append(uniqueConstraints, uniqueConstraint)
+ }
+
+ return &sqlschema.Table{
+ Name: tableName,
+ Columns: columns,
+ PrimaryKeyConstraint: primaryKeyConstraint,
+ ForeignKeyConstraints: foreignKeyConstraints,
+ }, uniqueConstraints, nil
+}
+
+func (provider *provider) GetIndices(ctx context.Context, name sqlschema.TableName) ([]sqlschema.Index, error) {
+ rows, err := provider.
+ sqlstore.
+ BunDB().
+ QueryContext(ctx, `
+SELECT
+ ct.relname AS table_name,
+ ci.relname AS index_name,
+ i.indisunique AS unique,
+ i.indisprimary AS primary,
+ a.attname AS column_name
+FROM
+ pg_index i
+ LEFT JOIN pg_class ct ON ct.oid = i.indrelid
+ LEFT JOIN pg_class ci ON ci.oid = i.indexrelid
+ LEFT JOIN pg_attribute a ON a.attrelid = ct.oid
+ LEFT JOIN pg_constraint con ON con.conindid = i.indexrelid
+WHERE
+ a.attnum = ANY(i.indkey)
+ AND con.oid IS NULL
+ AND ct.relkind = 'r'
+ AND ct.relname = ?`, string(name))
+ if err != nil {
+ return nil, err
+ }
+
+ defer func() {
+ if err := rows.Close(); err != nil {
+ provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
+ }
+ }()
+
+ uniqueIndicesMap := make(map[string]*sqlschema.UniqueIndex)
+ for rows.Next() {
+ var (
+ tableName string
+ indexName string
+ unique bool
+ primary bool
+ columnName string
+ )
+
+ if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName); err != nil {
+ return nil, err
+ }
+
+ if unique {
+ if _, ok := uniqueIndicesMap[indexName]; !ok {
+ uniqueIndicesMap[indexName] = &sqlschema.UniqueIndex{
+ TableName: name,
+ ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
+ }
+ } else {
+ uniqueIndicesMap[indexName].ColumnNames = append(uniqueIndicesMap[indexName].ColumnNames, sqlschema.ColumnName(columnName))
+ }
+ }
+ }
+
+ indices := make([]sqlschema.Index, 0)
+ for _, index := range uniqueIndicesMap {
+ indices = append(indices, index)
+ }
+
+ return indices, nil
+}
+
+func (provider *provider) ToggleFKEnforcement(_ context.Context, _ bun.IDB, _ bool) error {
+ return nil
+}
diff --git a/ee/sqlstore/postgressqlstore/dialect.go b/ee/sqlstore/postgressqlstore/dialect.go
index 7edd48dd677..4f6987bda77 100644
--- a/ee/sqlstore/postgressqlstore/dialect.go
+++ b/ee/sqlstore/postgressqlstore/dialect.go
@@ -17,19 +17,21 @@ var (
)
var (
- Org = "org"
- User = "user"
- UserNoCascade = "user_no_cascade"
- FactorPassword = "factor_password"
- CloudIntegration = "cloud_integration"
+ Org = "org"
+ User = "user"
+ UserNoCascade = "user_no_cascade"
+ FactorPassword = "factor_password"
+ CloudIntegration = "cloud_integration"
+ AgentConfigVersion = "agent_config_version"
)
var (
- OrgReference = `("org_id") REFERENCES "organizations" ("id")`
- UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
- UserReferenceNoCascade = `("user_id") REFERENCES "users" ("id")`
- FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
- CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
+ OrgReference = `("org_id") REFERENCES "organizations" ("id")`
+ UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
+ UserReferenceNoCascade = `("user_id") REFERENCES "users" ("id")`
+ FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
+ CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
+ AgentConfigVersionReference = `("version_id") REFERENCES "agent_config_version" ("id")`
)
type dialect struct{}
@@ -274,6 +276,8 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
fkReferences = append(fkReferences, FactorPasswordReference)
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
fkReferences = append(fkReferences, CloudIntegrationReference)
+ } else if reference == AgentConfigVersion && !slices.Contains(fkReferences, AgentConfigVersionReference) {
+ fkReferences = append(fkReferences, AgentConfigVersionReference)
}
}
diff --git a/ee/sqlstore/postgressqlstore/provider.go b/ee/sqlstore/postgressqlstore/provider.go
index dd2797a1faa..7e3bd22f34e 100644
--- a/ee/sqlstore/postgressqlstore/provider.go
+++ b/ee/sqlstore/postgressqlstore/provider.go
@@ -10,7 +10,6 @@ import (
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
- "github.com/jmoiron/sqlx"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
)
@@ -19,7 +18,6 @@ type provider struct {
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
- sqlxdb *sqlx.DB
dialect *dialect
}
@@ -61,7 +59,6 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
settings: settings,
sqldb: sqldb,
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
- sqlxdb: sqlx.NewDb(sqldb, "postgres"),
dialect: new(dialect),
}, nil
}
@@ -74,10 +71,6 @@ func (provider *provider) SQLDB() *sql.DB {
return provider.sqldb
}
-func (provider *provider) SQLxDB() *sqlx.DB {
- return provider.sqlxdb
-}
-
func (provider *provider) Dialect() sqlstore.SQLDialect {
return provider.dialect
}
diff --git a/frontend/.cursorrules b/frontend/.cursorrules
new file mode 100644
index 00000000000..9cfa908ba60
--- /dev/null
+++ b/frontend/.cursorrules
@@ -0,0 +1,484 @@
+# Persona
+You are an expert developer with deep knowledge of Jest, React Testing Library, MSW, and TypeScript, tasked with creating unit tests for this repository.
+
+# Auto-detect TypeScript Usage
+Check for TypeScript in the project through tsconfig.json or package.json dependencies.
+Adjust syntax based on this detection.
+
+# TypeScript Type Safety for Jest Tests
+**CRITICAL**: All Jest tests MUST be fully type-safe with proper TypeScript types.
+
+**Type Safety Requirements:**
+- Use proper TypeScript interfaces for all mock data
+- Type all Jest mock functions with `jest.MockedFunction`
+- Use generic types for React components and hooks
+- Define proper return types for mock functions
+- Use `as const` for literal types when needed
+- Avoid `any` type – use proper typing instead
+
+# Unit Testing Focus
+Focus on critical functionality (business logic, utility functions, component behavior)
+Mock dependencies (API calls, external modules) before imports
+Test multiple data scenarios (valid inputs, invalid inputs, edge cases)
+Write maintainable tests with descriptive names grouped in describe blocks
+
+# Global vs Local Mocks
+**Use Global Mocks for:**
+- High-frequency dependencies (20+ test files)
+- Core infrastructure (react-router-dom, react-query, antd)
+- Standard implementations across the app
+- Browser APIs (ResizeObserver, matchMedia, localStorage)
+- Utility libraries (date-fns, lodash)
+
+**Use Local Mocks for:**
+- Business logic dependencies (5-15 test files)
+- Test-specific behavior (different data per test)
+- API endpoints with specific responses
+- Domain-specific components
+- Error scenarios and edge cases
+
+**Global Mock Files Available (from jest.config.ts):**
+- `uplot` → `__mocks__/uplotMock.ts`
+
+# Repo-specific Testing Conventions
+
+## Imports
+Always import from our harness:
+```ts
+import { render, screen, userEvent, waitFor } from 'tests/test-utils';
+```
+For API mocks:
+```ts
+import { server, rest } from 'mocks-server/server';
+```
+Do not import directly from `@testing-library/react`.
+
+## Router
+Use the router built into render:
+```ts
+render(, undefined, { initialRoute: '/traces-explorer' });
+```
+Only mock `useLocation` / `useParams` if the test depends on them.
+
+## Hook Mocks
+Pattern:
+```ts
+import useFoo from 'hooks/useFoo';
+jest.mock('hooks/useFoo');
+const mockUseFoo = jest.mocked(useFoo);
+mockUseFoo.mockReturnValue(/* minimal shape */ as any);
+```
+Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
+
+## MSW
+Global MSW server runs automatically.
+Override per-test:
+```ts
+server.use(
+ rest.get('*/api/v1/foo', (_req, res, ctx) => res(ctx.status(200), ctx.json({ ok: true })))
+);
+```
+Keep large responses in `mocks-server/__mockdata_`.
+
+## Interactions
+- Prefer `userEvent` for real user interactions (click, type, select, tab).
+- Use `fireEvent` only for low-level/programmatic events not covered by `userEvent` (e.g., scroll, resize, setting `element.scrollTop` for virtualization). Wrap in `act(...)` if needed.
+- Always await interactions:
+```ts
+const user = userEvent.setup({ pointerEventsCheck: 0 });
+await user.click(screen.getByRole('button', { name: /save/i }));
+```
+
+```ts
+// Example: virtualized list scroll (no userEvent helper)
+const scroller = container.querySelector('[data-test-id="virtuoso-scroller"]') as HTMLElement;
+scroller.scrollTop = targetScrollTop;
+act(() => { fireEvent.scroll(scroller); });
+```
+
+## Timers
+❌ No global fake timers.
+✅ Per-test only, for debounce/throttle:
+```ts
+jest.useFakeTimers();
+const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
+await user.type(screen.getByRole('textbox'), 'query');
+jest.advanceTimersByTime(400);
+jest.useRealTimers();
+```
+
+## Queries
+Prefer accessible queries (`getByRole`, `findByRole`, `getByLabelText`).
+Fallback: visible text.
+Last resort: `data-testid`.
+
+# Example Test (using only configured global mocks)
+```ts
+import { render, screen, userEvent, waitFor } from 'tests/test-utils';
+import { server, rest } from 'mocks-server/server';
+import MyComponent from '../MyComponent';
+
+describe('MyComponent', () => {
+ it('renders and interacts', async () => {
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+
+ server.use(
+ rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
+ );
+
+ render(, undefined, { initialRoute: '/foo' });
+
+ expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: /refresh/i }));
+ await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
+ });
+});
+```
+
+# Anti-patterns
+❌ Importing RTL directly
+❌ Using global fake timers
+❌ Wrapping render in `act(...)`
+❌ Mocking infra dependencies locally (router, react-query)
+✅ Use our harness (`tests/test-utils`)
+✅ Use MSW for API overrides
+✅ Use userEvent + await
+✅ Pin time only in tests that assert relative dates
+
+# Best Practices
+- **Critical Functionality**: Prioritize testing business logic and utilities
+- **Dependency Mocking**: Global mocks for infra, local mocks for business logic
+- **Data Scenarios**: Always test valid, invalid, and edge cases
+- **Descriptive Names**: Make test intent clear
+- **Organization**: Group related tests in describe
+- **Consistency**: Match repo conventions
+- **Edge Cases**: Test null, undefined, unexpected values
+- **Limit Scope**: 3–5 focused tests per file
+- **Use Helpers**: `rqSuccess`, `makeUser`, etc.
+- **No Any**: Enforce type safety
+
+# Example Test
+```ts
+import { render, screen, userEvent, waitFor } from 'tests/test-utils';
+import { server, rest } from 'mocks-server/server';
+import MyComponent from '../MyComponent';
+
+describe('MyComponent', () => {
+ it('renders and interacts', async () => {
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+
+ server.use(
+ rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
+ );
+
+ render(, undefined, { initialRoute: '/foo' });
+
+ expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: /refresh/i }));
+ await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
+ });
+});
+```
+
+# Anti-patterns
+❌ Importing RTL directly
+❌ Using global fake timers
+❌ Wrapping render in `act(...)`
+❌ Mocking infra dependencies locally (router, react-query)
+✅ Use our harness (`tests/test-utils`)
+✅ Use MSW for API overrides
+✅ Use userEvent + await
+✅ Pin time only in tests that assert relative dates
+
+# TypeScript Type Safety Examples
+
+## Proper Mock Typing
+```ts
+// ✅ GOOD - Properly typed mocks
+interface User {
+ id: number;
+ name: string;
+ email: string;
+}
+
+interface ApiResponse {
+ data: T;
+ status: number;
+ message: string;
+}
+
+// Type the mock functions
+const mockFetchUser = jest.fn() as jest.MockedFunction<(id: number) => Promise>>;
+const mockUpdateUser = jest.fn() as jest.MockedFunction<(user: User) => Promise>>;
+
+// Mock implementation with proper typing
+mockFetchUser.mockResolvedValue({
+ data: { id: 1, name: 'John Doe', email: 'john@example.com' },
+ status: 200,
+ message: 'Success'
+});
+
+// ❌ BAD - Using any type
+const mockFetchUser = jest.fn() as any; // Don't do this
+```
+
+## React Component Testing with Types
+```ts
+// ✅ GOOD - Properly typed component testing
+interface ComponentProps {
+ title: string;
+ data: User[];
+ onUserSelect: (user: User) => void;
+ isLoading?: boolean;
+}
+
+const TestComponent: React.FC = ({ title, data, onUserSelect, isLoading = false }) => {
+ // Component implementation
+};
+
+describe('TestComponent', () => {
+ it('should render with proper props', () => {
+ // Arrange - Type the props properly
+ const mockProps: ComponentProps = {
+ title: 'Test Title',
+ data: [{ id: 1, name: 'John', email: 'john@example.com' }],
+ onUserSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
+ isLoading: false
+ };
+
+ // Act
+ render();
+
+ // Assert
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
+ });
+});
+```
+
+## Hook Testing with Types
+```ts
+// ✅ GOOD - Properly typed hook testing
+interface UseUserDataReturn {
+ user: User | null;
+ loading: boolean;
+ error: string | null;
+ refetch: () => void;
+}
+
+const useUserData = (id: number): UseUserDataReturn => {
+ // Hook implementation
+};
+
+describe('useUserData', () => {
+ it('should return user data with proper typing', () => {
+ // Arrange
+ const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
+ mockFetchUser.mockResolvedValue({
+ data: mockUser,
+ status: 200,
+ message: 'Success'
+ });
+
+ // Act
+ const { result } = renderHook(() => useUserData(1));
+
+ // Assert
+ expect(result.current.user).toEqual(mockUser);
+ expect(result.current.loading).toBe(false);
+ expect(result.current.error).toBeNull();
+ });
+});
+```
+
+## Global Mock Type Safety
+```ts
+// ✅ GOOD - Type-safe global mocks
+// In __mocks__/routerMock.ts
+export const mockUseLocation = (overrides: Partial = {}): Location => ({
+ pathname: '/traces',
+ search: '',
+ hash: '',
+ state: null,
+ key: 'test-key',
+ ...overrides,
+});
+
+// In test files
+const location = useLocation(); // Properly typed from global mock
+expect(location.pathname).toBe('/traces');
+```
+
+# TypeScript Configuration for Jest
+
+## Required Jest Configuration
+```json
+// jest.config.ts
+{
+ "preset": "ts-jest/presets/js-with-ts-esm",
+ "globals": {
+ "ts-jest": {
+ "useESM": true,
+ "isolatedModules": true,
+ "tsconfig": "/tsconfig.jest.json"
+ }
+ },
+ "extensionsToTreatAsEsm": [".ts", ".tsx"],
+ "moduleFileExtensions": ["ts", "tsx", "js", "json"]
+}
+```
+
+## TypeScript Jest Configuration
+```json
+// tsconfig.jest.json
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "types": ["jest", "@testing-library/jest-dom"],
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "moduleResolution": "node"
+ },
+ "include": [
+ "src/**/*",
+ "**/*.test.ts",
+ "**/*.test.tsx",
+ "__mocks__/**/*"
+ ]
+}
+```
+
+## Common Type Safety Patterns
+
+### Mock Function Typing
+```ts
+// ✅ GOOD - Proper mock function typing
+const mockApiCall = jest.fn() as jest.MockedFunction;
+const mockEventHandler = jest.fn() as jest.MockedFunction<(event: Event) => void>;
+
+// ❌ BAD - Using any
+const mockApiCall = jest.fn() as any;
+```
+
+### Generic Mock Typing
+```ts
+// ✅ GOOD - Generic mock typing
+interface MockApiResponse {
+ data: T;
+ status: number;
+}
+
+const mockFetchData = jest.fn() as jest.MockedFunction<
+ (endpoint: string) => Promise>
+>;
+
+// Usage
+mockFetchData('/users').mockResolvedValue({
+ data: { id: 1, name: 'John' },
+ status: 200
+});
+```
+
+### React Testing Library with Types
+```ts
+// ✅ GOOD - Typed testing utilities
+import { render, screen, RenderResult } from '@testing-library/react';
+import { ComponentProps } from 'react';
+
+type TestComponentProps = ComponentProps;
+
+const renderTestComponent = (props: Partial = {}): RenderResult => {
+ const defaultProps: TestComponentProps = {
+ title: 'Test',
+ data: [],
+ onSelect: jest.fn(),
+ ...props
+ };
+
+ return render();
+};
+```
+
+### Error Handling with Types
+```ts
+// ✅ GOOD - Typed error handling
+interface ApiError {
+ message: string;
+ code: number;
+ details?: Record;
+}
+
+const mockApiError: ApiError = {
+ message: 'API Error',
+ code: 500,
+ details: { endpoint: '/users' }
+};
+
+mockFetchUser.mockRejectedValue(new Error(JSON.stringify(mockApiError)));
+```
+
+## Type Safety Checklist
+- [ ] All mock functions use `jest.MockedFunction`
+- [ ] All mock data has proper interfaces
+- [ ] No `any` types in test files
+- [ ] Generic types are used where appropriate
+- [ ] Error types are properly defined
+- [ ] Component props are typed
+- [ ] Hook return types are defined
+- [ ] API response types are defined
+- [ ] Global mocks are type-safe
+- [ ] Test utilities are properly typed
+
+# Mock Decision Tree
+```
+Is it used in 20+ test files?
+├─ YES → Use Global Mock
+│ ├─ react-router-dom
+│ ├─ react-query
+│ ├─ antd components
+│ └─ browser APIs
+│
+└─ NO → Is it business logic?
+ ├─ YES → Use Local Mock
+ │ ├─ API endpoints
+ │ ├─ Custom hooks
+ │ └─ Domain components
+ │
+ └─ NO → Is it test-specific?
+ ├─ YES → Use Local Mock
+ │ ├─ Error scenarios
+ │ ├─ Loading states
+ │ └─ Specific data
+ │
+ └─ NO → Consider Global Mock
+ └─ If it becomes frequently used
+```
+
+# Common Anti-Patterns to Avoid
+
+❌ **Don't mock global dependencies locally:**
+```js
+// BAD - This is already globally mocked
+jest.mock('react-router-dom', () => ({ ... }));
+```
+
+❌ **Don't create global mocks for test-specific data:**
+```js
+// BAD - This should be local
+jest.mock('../api/tracesService', () => ({
+ getTraces: jest.fn(() => specificTestData)
+}));
+```
+
+✅ **Do use global mocks for infrastructure:**
+```js
+// GOOD - Use global mock
+import { useLocation } from 'react-router-dom';
+```
+
+✅ **Do create local mocks for business logic:**
+```js
+// GOOD - Local mock for specific test needs
+jest.mock('../api/tracesService', () => ({
+ getTraces: jest.fn(() => mockTracesData)
+}));
+```
\ No newline at end of file
diff --git a/frontend/.eslintignore b/frontend/.eslintignore
index 402f7ae028e..e9d38dfe02c 100644
--- a/frontend/.eslintignore
+++ b/frontend/.eslintignore
@@ -1,4 +1,5 @@
node_modules
build
*.typegen.ts
-i18-generate-hash.js
\ No newline at end of file
+i18-generate-hash.js
+src/parser/TraceOperatorParser/**
\ No newline at end of file
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
index f0f5a7330a6..25ff23f209e 100644
--- a/frontend/.eslintrc.js
+++ b/frontend/.eslintrc.js
@@ -1,4 +1,5 @@
module.exports = {
+ ignorePatterns: ['src/parser/*.ts'],
env: {
browser: true,
es2021: true,
diff --git a/frontend/.gitignore b/frontend/.gitignore
index a5489639406..cfd8dac366f 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -2,3 +2,30 @@
# Sentry Config File
.env.sentry-build-plugin
.qodo
+
+# Playwright
+node_modules/
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+/playwright/test-results/
+/playwright/blob-report/
+/playwright/playwright-report/
+
+e2e/test-plan/alerts/
+e2e/test-plan/dashboards/
+e2e/test-plan/exceptions/
+e2e/test-plan/external-apis/
+e2e/test-plan/help-support/
+e2e/test-plan/infrastructure/
+e2e/test-plan/logs/
+e2e/test-plan/messaging-queues/
+e2e/test-plan/metrics/
+e2e/test-plan/navigation/
+e2e/test-plan/onboarding/
+e2e/test-plan/saved-views/
+e2e/test-plan/service-map/
+e2e/test-plan/services/
+e2e/test-plan/traces/
+e2e/test-plan/user-preferences/
\ No newline at end of file
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
index 30573bc2829..0b802be4b4e 100644
--- a/frontend/.prettierignore
+++ b/frontend/.prettierignore
@@ -8,3 +8,8 @@ public/
# Ignore all JSON files:
**/*.json
+
+# Ignore all files in parser folder:
+src/parser/**
+
+src/TraceOperator/parser/**
\ No newline at end of file
diff --git a/frontend/__mocks__/uplotMock.ts b/frontend/__mocks__/uplotMock.ts
new file mode 100644
index 00000000000..9cf9add9f0c
--- /dev/null
+++ b/frontend/__mocks__/uplotMock.ts
@@ -0,0 +1,51 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+
+// Mock for uplot library used in tests
+export interface MockUPlotInstance {
+ setData: jest.Mock;
+ setSize: jest.Mock;
+ destroy: jest.Mock;
+ redraw: jest.Mock;
+ setSeries: jest.Mock;
+}
+
+export interface MockUPlotPaths {
+ spline: jest.Mock;
+ bars: jest.Mock;
+}
+
+// Create mock instance methods
+const createMockUPlotInstance = (): MockUPlotInstance => ({
+ setData: jest.fn(),
+ setSize: jest.fn(),
+ destroy: jest.fn(),
+ redraw: jest.fn(),
+ setSeries: jest.fn(),
+});
+
+// Create mock paths
+const mockPaths: MockUPlotPaths = {
+ spline: jest.fn(),
+ bars: jest.fn(),
+};
+
+// Mock static methods
+const mockTzDate = jest.fn(
+ (date: Date, _timezone: string) => new Date(date.getTime()),
+);
+
+// Mock uPlot constructor - this needs to be a proper constructor function
+function MockUPlot(
+ _options: unknown,
+ _data: unknown,
+ _target: HTMLElement,
+): MockUPlotInstance {
+ return createMockUPlotInstance();
+}
+
+// Add static methods to the constructor
+MockUPlot.tzDate = mockTzDate;
+MockUPlot.paths = mockPaths;
+
+// Export the constructor as default
+export default MockUPlot;
diff --git a/frontend/__mocks__/useSafeNavigate.ts b/frontend/__mocks__/useSafeNavigate.ts
new file mode 100644
index 00000000000..a1044da052c
--- /dev/null
+++ b/frontend/__mocks__/useSafeNavigate.ts
@@ -0,0 +1,29 @@
+// Mock for useSafeNavigate hook to avoid React Router version conflicts in tests
+interface SafeNavigateOptions {
+ replace?: boolean;
+ state?: unknown;
+}
+
+interface SafeNavigateTo {
+ pathname?: string;
+ search?: string;
+ hash?: string;
+}
+
+type SafeNavigateToType = string | SafeNavigateTo;
+
+interface UseSafeNavigateReturn {
+ safeNavigate: jest.MockedFunction<
+ (to: SafeNavigateToType, options?: SafeNavigateOptions) => void
+ >;
+}
+
+export const useSafeNavigate = (): UseSafeNavigateReturn => ({
+ safeNavigate: jest.fn(
+ (to: SafeNavigateToType, options?: SafeNavigateOptions) => {
+ console.log(`Mock safeNavigate called with:`, to, options);
+ },
+ ) as jest.MockedFunction<
+ (to: SafeNavigateToType, options?: SafeNavigateOptions) => void
+ >,
+});
diff --git a/frontend/e2e/test-plan/README.md b/frontend/e2e/test-plan/README.md
new file mode 100644
index 00000000000..6853834d189
--- /dev/null
+++ b/frontend/e2e/test-plan/README.md
@@ -0,0 +1,29 @@
+# SigNoz E2E Test Plan
+
+This directory contains the structured test plan for the SigNoz application. Each subfolder corresponds to a main module or feature area, and contains scenario files for all user journeys, edge cases, and cross-module flows. These documents serve as the basis for generating Playwright MCP-driven E2E tests.
+
+## Structure
+
+- Each main module (e.g., logs, traces, dashboards, alerts, settings, etc.) has its own folder or markdown file.
+- Each file contains detailed scenario templates, including preconditions, step-by-step actions, and expected outcomes.
+- Use these documents to write, review, and update test cases as the application evolves.
+
+## Folders & Files
+
+- `logs/` — Logs module scenarios
+- `traces/` — Traces module scenarios
+- `metrics/` — Metrics module scenarios
+- `dashboards/` — Dashboards module scenarios
+- `alerts/` — Alerts module scenarios
+- `services/` — Services module scenarios
+- `settings/` — Settings and all sub-settings scenarios
+- `onboarding/` — Onboarding and signup flows
+- `navigation/` — Navigation, sidebar, and cross-module flows
+- `exceptions/` — Exception and error handling scenarios
+- `external-apis/` — External API monitoring scenarios
+- `messaging-queues/` — Messaging queue scenarios
+- `infrastructure/` — Infrastructure monitoring scenarios
+- `help-support/` — Help & support scenarios
+- `user-preferences/` — User preferences and personalization scenarios
+- `service-map/` — Service map scenarios
+- `saved-views/` — Saved views scenarios
diff --git a/frontend/e2e/test-plan/settings/README.md b/frontend/e2e/test-plan/settings/README.md
new file mode 100644
index 00000000000..2e424598e9d
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/README.md
@@ -0,0 +1,16 @@
+# Settings Module Test Plan
+
+This folder contains E2E test scenarios for the Settings module and all sub-settings.
+
+## Scenario Categories
+
+- General settings (org/workspace, branding, version info)
+- Billing settings
+- Members & SSO
+- Custom domain
+- Integrations
+- Notification channels
+- API keys
+- Ingestion
+- Account settings (profile, password, preferences)
+- Keyboard shortcuts
diff --git a/frontend/e2e/test-plan/settings/account-settings.md b/frontend/e2e/test-plan/settings/account-settings.md
new file mode 100644
index 00000000000..63677d662f8
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/account-settings.md
@@ -0,0 +1,43 @@
+# Account Settings E2E Scenarios (Updated)
+
+## 1. Update Name
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. Click 'Update name' button
+ 2. Edit name field in the modal/dialog
+ 3. Save changes
+- **Expected:** Name is updated in the UI
+
+## 2. Update Email
+
+- **Note:** The email field is not editable in the current UI.
+
+## 3. Reset Password
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. Click 'Reset password' button
+ 2. Complete reset flow (modal/dialog or external flow)
+- **Expected:** Password is reset
+
+## 4. Toggle 'Adapt to my timezone'
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. Toggle 'Adapt to my timezone' switch
+- **Expected:** Timezone adapts accordingly (UI feedback/confirmation should be checked)
+
+## 5. Toggle Theme (Dark/Light)
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. Toggle theme radio buttons ('Dark', 'Light Beta')
+- **Expected:** Theme changes
+
+## 6. Toggle Sidebar Always Open
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. Toggle 'Keep the primary sidebar always open' switch
+- **Expected:** Sidebar remains open/closed as per toggle
diff --git a/frontend/e2e/test-plan/settings/api-keys.md b/frontend/e2e/test-plan/settings/api-keys.md
new file mode 100644
index 00000000000..6e2b81336f0
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/api-keys.md
@@ -0,0 +1,26 @@
+# API Keys E2E Scenarios (Updated)
+
+## 1. Create a New API Key
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Click 'New Key' button
+ 2. Enter details in the modal/dialog
+ 3. Click 'Save'
+- **Expected:** API key is created and listed in the table
+
+## 2. Revoke an API Key
+
+- **Precondition:** API key exists
+- **Steps:**
+ 1. In the table, locate the API key row
+ 2. Click the revoke/delete button (icon button in the Action column)
+ 3. Confirm if prompted
+- **Expected:** API key is revoked/removed from the table
+
+## 3. View API Key Usage
+
+- **Precondition:** API key exists
+- **Steps:**
+ 1. View the 'Last used' and 'Expired' columns in the table
+- **Expected:** Usage data is displayed for each API key
diff --git a/frontend/e2e/test-plan/settings/billing.md b/frontend/e2e/test-plan/settings/billing.md
new file mode 100644
index 00000000000..083fe6a6b2c
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/billing.md
@@ -0,0 +1,17 @@
+# Billing Settings E2E Scenarios (Updated)
+
+## 1. View Billing Information
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Navigate to Billing Settings
+ 2. Wait for the billing chart/data to finish loading
+- **Expected:**
+ - Billing heading and subheading are displayed
+ - Usage/cost table is visible with columns: Unit, Data Ingested, Price per Unit, Cost (Billing period to date)
+ - "Download CSV" and "Manage Billing" buttons are present and enabled after loading
+ - Test clicking "Download CSV" and "Manage Billing" for expected behavior (e.g., file download, navigation, or modal)
+
+> Note: If these features are expected to trigger specific flows, document the observed behavior for each button.
+
+
diff --git a/frontend/e2e/test-plan/settings/custom-domain.md b/frontend/e2e/test-plan/settings/custom-domain.md
new file mode 100644
index 00000000000..ba58876ff83
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/custom-domain.md
@@ -0,0 +1,18 @@
+# Custom Domain E2E Scenarios (Updated)
+
+## 1. Add or Update Custom Domain
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Click 'Customize team’s URL' button
+ 2. In the 'Customize your team’s URL' dialog, enter the preferred subdomain
+ 3. Click 'Apply Changes'
+- **Expected:** Domain is set/updated for the team (UI feedback/confirmation should be checked)
+
+## 2. Verify Domain Ownership
+
+- **Note:** No explicit 'Verify' button or flow is present in the current UI. If verification is required, it may be handled automatically or via support.
+
+## 3. Remove a Custom Domain
+
+- **Note:** No explicit 'Remove' button or flow is present in the current UI. The only available action is to update the subdomain.
diff --git a/frontend/e2e/test-plan/settings/general.md b/frontend/e2e/test-plan/settings/general.md
new file mode 100644
index 00000000000..58786dd8313
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/general.md
@@ -0,0 +1,31 @@
+# General Settings E2E Scenarios
+
+## 1. View General Settings
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. Navigate to General Settings
+- **Expected:** General settings are displayed
+
+## 2. Update Organization/Workspace Name
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Edit organization/workspace name
+ 2. Save changes
+- **Expected:** Name is updated and visible
+
+## 3. Update Logo or Branding
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Upload new logo/branding
+ 2. Save changes
+- **Expected:** Branding is updated
+
+## 4. View Version/Build Info
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. View version/build info section
+- **Expected:** Version/build info is displayed
diff --git a/frontend/e2e/test-plan/settings/ingestion.md b/frontend/e2e/test-plan/settings/ingestion.md
new file mode 100644
index 00000000000..96a600bb716
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/ingestion.md
@@ -0,0 +1,20 @@
+# Ingestion E2E Scenarios (Updated)
+
+## 1. View Ingestion Sources
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Navigate to the Integrations page
+- **Expected:** List of available data sources/integrations is displayed
+
+## 2. Configure Ingestion Sources
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Click 'Configure' for a data source/integration
+ 2. Complete the configuration flow (modal or page, as available)
+- **Expected:** Source is configured (UI feedback/confirmation should be checked)
+
+## 3. Disable/Enable Ingestion
+
+- **Note:** No visible enable/disable toggle for ingestion sources in the current UI. Ingestion is managed via the Integrations configuration flows.
diff --git a/frontend/e2e/test-plan/settings/integrations.md b/frontend/e2e/test-plan/settings/integrations.md
new file mode 100644
index 00000000000..1d6bfdda702
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/integrations.md
@@ -0,0 +1,51 @@
+# Integrations E2E Scenarios (Updated)
+
+## 1. View List of Available Integrations
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. Navigate to Integrations
+- **Expected:** List of integrations is displayed, each with a name, description, and 'Configure' button
+
+## 2. Search Integrations by Name/Type
+
+- **Precondition:** Integrations exist
+- **Steps:**
+ 1. Enter search/filter criteria in the 'Search for an integration...' box
+- **Expected:** Only matching integrations are shown
+
+## 3. Connect a New Integration
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Click 'Configure' for an integration
+ 2. Complete the configuration flow (modal or page, as available)
+- **Expected:** Integration is connected/configured (UI feedback/confirmation should be checked)
+
+## 4. Disconnect an Integration
+
+- **Note:** No visible 'Disconnect' button in the main list. This may be available in the configuration flow for a connected integration.
+
+## 5. Configure Integration Settings
+
+- **Note:** Configuration is handled in the flow after clicking 'Configure' for an integration.
+
+## 6. Test Integration Connection
+
+- **Note:** No visible 'Test Connection' button in the main list. This may be available in the configuration flow.
+
+## 7. View Integration Status/Logs
+
+- **Note:** No visible status/logs section in the main list. This may be available in the configuration flow.
+
+## 8. Filter Integrations by Category
+
+- **Note:** No explicit category filter in the current UI, only a search box.
+
+## 9. View Integration Documentation/Help
+
+- **Note:** No visible 'Help/Docs' button in the main list. This may be available in the configuration flow.
+
+## 10. Update Integration Configuration
+
+- **Note:** Configuration is handled in the flow after clicking 'Configure' for an integration.
diff --git a/frontend/e2e/test-plan/settings/keyboard-shortcuts.md b/frontend/e2e/test-plan/settings/keyboard-shortcuts.md
new file mode 100644
index 00000000000..753624e4670
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/keyboard-shortcuts.md
@@ -0,0 +1,19 @@
+# Keyboard Shortcuts E2E Scenarios (Updated)
+
+## 1. View Keyboard Shortcuts
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. Navigate to Keyboard Shortcuts
+- **Expected:** Shortcuts are displayed in categorized tables (Global, Logs Explorer, Query Builder, Dashboard)
+
+## 2. Customize Keyboard Shortcuts (if supported)
+
+- **Note:** Customization is not available in the current UI. Shortcuts are view-only.
+
+## 3. Use Keyboard Shortcuts for Navigation/Actions
+
+- **Precondition:** User is logged in
+- **Steps:**
+ 1. Use shortcut for navigation/action (e.g., shift+s for Services, cmd+enter for running query)
+- **Expected:** Navigation/action is performed as per shortcut
diff --git a/frontend/e2e/test-plan/settings/members-sso.md b/frontend/e2e/test-plan/settings/members-sso.md
new file mode 100644
index 00000000000..8fd84f391c7
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/members-sso.md
@@ -0,0 +1,49 @@
+# Members & SSO E2E Scenarios (Updated)
+
+## 1. Invite a New Member
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Click 'Invite Members' button
+ 2. In the 'Invite team members' dialog, enter email address, name (optional), and select role
+ 3. (Optional) Click 'Add another team member' to invite more
+ 4. Click 'Invite team members' to send invite(s)
+- **Expected:** Pending invite appears in the 'Pending Invites' table
+
+## 2. Remove a Member
+
+- **Precondition:** User is admin, member exists
+- **Steps:**
+ 1. In the 'Members' table, locate the member row
+ 2. Click 'Delete' in the Action column
+ 3. Confirm removal if prompted
+- **Expected:** Member is removed from the table
+
+## 3. Update Member Roles
+
+- **Precondition:** User is admin, member exists
+- **Steps:**
+ 1. In the 'Members' table, locate the member row
+ 2. Click 'Edit' in the Action column
+ 3. Change role in the edit dialog/modal
+ 4. Save changes
+- **Expected:** Member role is updated in the table
+
+## 4. Configure SSO
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. In the 'Authenticated Domains' section, locate the domain row
+ 2. Click 'Configure SSO' or 'Edit Google Auth' as available
+ 3. Complete SSO provider configuration in the modal/dialog
+ 4. Save settings
+- **Expected:** SSO is configured for the domain
+
+## 5. Login via SSO
+
+- **Precondition:** SSO is configured
+- **Steps:**
+ 1. Log out from the app
+ 2. On the login page, click 'Login with SSO'
+ 3. Complete SSO login flow
+- **Expected:** User is logged in via SSO
diff --git a/frontend/e2e/test-plan/settings/notification-channels.md b/frontend/e2e/test-plan/settings/notification-channels.md
new file mode 100644
index 00000000000..36e4098b8d5
--- /dev/null
+++ b/frontend/e2e/test-plan/settings/notification-channels.md
@@ -0,0 +1,39 @@
+# Notification Channels E2E Scenarios (Updated)
+
+## 1. Add a New Notification Channel
+
+- **Precondition:** User is admin
+- **Steps:**
+ 1. Click 'New Alert Channel' button
+ 2. In the 'New Notification Channel' form, fill in required fields (Name, Type, Webhook URL, etc.)
+ 3. (Optional) Toggle 'Send resolved alerts'
+ 4. (Optional) Click 'Test' to send a test notification
+ 5. Click 'Save' to add the channel
+- **Expected:** Channel is added and listed in the table
+
+## 2. Test Notification Channel
+
+- **Precondition:** Channel is being created or edited
+- **Steps:**
+ 1. In the 'New Notification Channel' or 'Edit Notification Channel' form, click 'Test'
+- **Expected:** Test notification is sent (UI feedback/confirmation should be checked)
+
+## 3. Remove a Notification Channel
+
+- **Precondition:** Channel is added
+- **Steps:**
+ 1. In the table, locate the channel row
+ 2. Click 'Delete' in the Action column
+ 3. Confirm removal if prompted
+- **Expected:** Channel is removed from the table
+
+## 4. Update Notification Channel Settings
+
+- **Precondition:** Channel is added
+- **Steps:**
+ 1. In the table, locate the channel row
+ 2. Click 'Edit' in the Action column
+ 3. In the 'Edit Notification Channel' form, update fields as needed
+ 4. (Optional) Click 'Test' to send a test notification
+ 5. Click 'Save' to update the channel
+- **Expected:** Settings are updated
diff --git a/frontend/e2e/test-plan/validation-report.md b/frontend/e2e/test-plan/validation-report.md
new file mode 100644
index 00000000000..29dd62f4bb6
--- /dev/null
+++ b/frontend/e2e/test-plan/validation-report.md
@@ -0,0 +1,199 @@
+# SigNoz Test Plan Validation Report
+
+This report documents the validation of the E2E test plan against the current live application using Playwright MCP. Each module is reviewed for coverage, gaps, and required updates.
+
+---
+
+## Home Module
+
+- **Coverage:**
+ - Widgets for logs, traces, metrics, dashboards, alerts, services, saved views, onboarding checklist
+ - Quick access buttons: Explore Logs, Create dashboard, Create an alert
+- **Gaps/Updates:**
+ - Add scenarios for checklist interactions (e.g., “I’ll do this later”, progress tracking)
+ - Add scenarios for Saved Views and cross-module links
+ - Add scenario for onboarding checklist completion
+
+---
+
+## Logs Module
+
+- **Coverage:**
+ - Explorer, Pipelines, Views tabs
+ - Filtering by service, environment, severity, host, k8s, etc.
+ - Search, save view, create alert, add to dashboard, export, view mode switching
+- **Gaps/Updates:**
+ - Add scenario for quick filter customization
+ - Add scenario for “Old Explorer” button
+ - Add scenario for frequency chart toggle
+ - Add scenario for “Stage & Run Query” workflow
+
+---
+
+## Traces Module
+
+- **Coverage:**
+ - Tabs: Explorer, Funnels, Views
+ - Filtering by name, error status, duration, environment, function, service, RPC, status code, HTTP, trace ID, etc.
+ - Search, save view, create alert, add to dashboard, export, view mode switching (List, Traces, Time Series, Table)
+ - Pagination, quick filter customization, group by, aggregation
+- **Gaps/Updates:**
+ - Add scenario for quick filter customization
+ - Add scenario for “Stage & Run Query” workflow
+ - Add scenario for all view modes (List, Traces, Time Series, Table)
+ - Add scenario for group by/aggregation
+ - Add scenario for trace detail navigation (clicking on trace row)
+ - Add scenario for Funnels tab (create/edit/delete funnel)
+ - Add scenario for Views tab (manage saved views)
+
+---
+
+## Metrics Module
+
+- **Coverage:**
+ - Tabs: Summary, Explorer, Views
+ - Filtering by metric, type, unit, etc.
+ - Search, save view, add to dashboard, export, view mode switching (chart, table, proportion view)
+ - Pagination, group by, aggregation, custom queries
+- **Gaps/Updates:**
+ - Add scenario for Proportion View in Summary
+ - Add scenario for all view modes (chart, table, proportion)
+ - Add scenario for group by/aggregation
+ - Add scenario for custom queries in Explorer
+ - Add scenario for Views tab (manage saved views)
+
+---
+
+## Dashboards Module
+
+- **Coverage:**
+ - List, search, and filter dashboards
+ - Create new dashboard (button and template link)
+ - Edit, delete, and view dashboard details
+ - Add/edit/delete widgets (implied by dashboard detail)
+ - Pagination through dashboards
+- **Gaps/Updates:**
+ - Add scenario for browsing dashboard templates (external link)
+ - Add scenario for requesting new template
+ - Add scenario for dashboard owner and creation info
+ - Add scenario for dashboard tags and filtering by tags
+ - Add scenario for dashboard sharing (if available)
+ - Add scenario for dashboard image/preview
+
+---
+
+## Messaging Queues Module
+
+- **Coverage:**
+ - Overview tab: queue metrics, filters (Service Name, Span Name, Msg System, Destination, Kind)
+ - Search across all columns
+ - Pagination of queue data
+ - Sync and Share buttons
+ - Tabs for Kafka and Celery
+- **Gaps/Updates:**
+ - Add scenario for Kafka tab (detailed metrics, actions)
+ - Add scenario for Celery tab (detailed metrics, actions)
+ - Add scenario for filter combinations and edge cases
+ - Add scenario for sharing queue data
+ - Add scenario for time range selection
+
+---
+
+## External APIs Module
+
+- **Coverage:**
+ - Accessed via side navigation under MORE
+ - Explorer tab: domain, endpoints, last used, rate, error %, avg. latency
+ - Filters: Deployment Environment, Service Name, Rpc Method, Show IP addresses
+ - Table pagination
+ - Share and Stage & Run Query buttons
+- **Gaps/Updates:**
+ - Add scenario for customizing quick filters
+ - Add scenario for running and staging queries
+ - Add scenario for sharing API data
+ - Add scenario for edge cases in filters and table data
+
+---
+
+## Alerts Module
+
+- **Coverage:**
+ - Alert Rules tab: list, search, create (New Alert), edit, delete, enable/disable, severity, labels, actions
+ - Triggered Alerts tab (visible in tablist)
+ - Configuration tab (visible in tablist)
+ - Table pagination
+- **Gaps/Updates:**
+ - Add scenario for triggered alerts (view, acknowledge, resolve)
+ - Add scenario for alert configuration (settings, integrations)
+ - Add scenario for edge cases in alert creation and management
+ - Add scenario for searching and filtering alerts
+
+---
+
+## Integrations Module
+
+- **Coverage:**
+ - Integrations tab: list, search, configure (e.g., AWS), request new integration
+ - One-click setup for AWS monitoring
+ - Request more integrations (form)
+- **Gaps/Updates:**
+ - Add scenario for configuring integrations (step-by-step)
+ - Add scenario for searching and filtering integrations
+ - Add scenario for requesting new integrations
+ - Add scenario for edge cases (e.g., failed configuration)
+
+---
+
+## Exceptions Module
+
+- **Coverage:**
+ - All Exceptions: list, search, filter (Deployment Environment, Service Name, Host Name, K8s Cluster/Deployment/Namespace, Net Peer Name)
+ - Table: Exception Type, Error Message, Count, Last Seen, First Seen, Application
+ - Pagination
+ - Exception detail links
+ - Share and Stage & Run Query buttons
+- **Gaps/Updates:**
+ - Add scenario for exception detail view
+ - Add scenario for advanced filtering and edge cases
+ - Add scenario for sharing and running queries
+ - Add scenario for error grouping and navigation
+
+---
+
+## Service Map Module
+
+- **Coverage:**
+ - Service Map visualization (main graph)
+ - Filters: environment, resource attributes
+ - Time range selection
+ - Sync and Share buttons
+- **Gaps/Updates:**
+ - Add scenario for interacting with the map (zoom, pan, select service)
+ - Add scenario for filtering and edge cases
+ - Add scenario for sharing the map
+ - Add scenario for time range and environment combinations
+
+---
+
+## Billing Module
+
+- **Coverage:**
+ - Billing overview: cost monitoring, invoices, CSV download (disabled), manage billing (disabled)
+ - Teams Cloud section
+ - Billing table: Unit, Data Ingested, Price per Unit, Cost (Billing period to date)
+- **Gaps/Updates:**
+ - Add scenario for invoice download and management (when enabled)
+ - Add scenario for cost monitoring and edge cases
+ - Add scenario for billing table data validation
+ - Add scenario for permissions and access control
+
+---
+
+## Usage Explorer Module
+
+- **Status:**
+ - Not accessible in the current environment. Removing from test plan flows.
+
+---
+
+## [Next modules will be filled as validation proceeds]
diff --git a/frontend/e2e/tests/settings/account-settings/account-settings-settings.spec.ts b/frontend/e2e/tests/settings/account-settings/account-settings-settings.spec.ts
new file mode 100644
index 00000000000..0cf15e66cec
--- /dev/null
+++ b/frontend/e2e/tests/settings/account-settings/account-settings-settings.spec.ts
@@ -0,0 +1,42 @@
+import { expect, test } from '@playwright/test';
+
+import { ensureLoggedIn } from '../../../utils/login.util';
+
+test('Account Settings - View and Assert Static Controls', async ({ page }) => {
+ await ensureLoggedIn(page);
+
+ // 1. Open the sidebar settings menu using data-testid
+ await page.getByTestId('settings-nav-item').click();
+
+ // 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
+ await page.getByRole('menuitem', { name: 'Account Settings' }).click();
+
+ // Assert the main tabpanel/heading (confirmed by DOM)
+ await expect(page.getByTestId('settings-page-title')).toBeVisible();
+
+ // Assert General section and controls (confirmed by DOM)
+ await expect(
+ page.getByLabel('My Settings').getByText('General'),
+ ).toBeVisible();
+ await expect(page.getByText('Manage your account settings.')).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Update name' })).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: 'Reset password' }),
+ ).toBeVisible();
+
+ // Assert User Preferences section and controls (confirmed by DOM)
+ await expect(page.getByText('User Preferences')).toBeVisible();
+ await expect(
+ page.getByText('Tailor the SigNoz console to work according to your needs.'),
+ ).toBeVisible();
+ await expect(page.getByText('Select your theme')).toBeVisible();
+
+ const themeSelector = page.getByTestId('theme-selector');
+
+ await expect(themeSelector.getByText('Dark')).toBeVisible();
+ await expect(themeSelector.getByText('Light')).toBeVisible();
+ await expect(themeSelector.getByText('System')).toBeVisible();
+
+ await expect(page.getByTestId('timezone-adaptation-switch')).toBeVisible();
+ await expect(page.getByTestId('side-nav-pinned-switch')).toBeVisible();
+});
diff --git a/frontend/e2e/tests/settings/api-keys/api-keys-settings.spec.ts b/frontend/e2e/tests/settings/api-keys/api-keys-settings.spec.ts
new file mode 100644
index 00000000000..e91742e47ae
--- /dev/null
+++ b/frontend/e2e/tests/settings/api-keys/api-keys-settings.spec.ts
@@ -0,0 +1,42 @@
+import { expect, test } from '@playwright/test';
+
+import { ensureLoggedIn } from '../../../utils/login.util';
+
+test('API Keys Settings - View and Interact', async ({ page }) => {
+ await ensureLoggedIn(page);
+
+ // 1. Open the sidebar settings menu using data-testid
+ await page.getByTestId('settings-nav-item').click();
+
+ // 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
+ await page.getByRole('menuitem', { name: 'Account Settings' }).click();
+
+ // Assert the main tabpanel/heading (confirmed by DOM)
+ await expect(page.getByTestId('settings-page-title')).toBeVisible();
+
+ // Focus on the settings page sidenav
+ await page.getByTestId('settings-page-sidenav').focus();
+
+ // Click API Keys tab in the settings sidebar (by data-testid)
+ await page.getByTestId('api-keys').click();
+
+ // Assert heading and subheading
+ await expect(page.getByRole('heading', { name: 'API Keys' })).toBeVisible();
+ await expect(
+ page.getByText('Create and manage API keys for the SigNoz API'),
+ ).toBeVisible();
+
+ // Assert presence of New Key button
+ const newKeyBtn = page.getByRole('button', { name: 'New Key' });
+ await expect(newKeyBtn).toBeVisible();
+
+ // Assert table columns
+ await expect(page.getByText('Last used').first()).toBeVisible();
+ await expect(page.getByText('Expired').first()).toBeVisible();
+
+ // Assert at least one API key row with action buttons
+ // Select the first action cell's first button (icon button)
+ const firstActionCell = page.locator('table tr').nth(1).locator('td').last();
+ const deleteBtn = firstActionCell.locator('button').first();
+ await expect(deleteBtn).toBeVisible();
+});
diff --git a/frontend/e2e/tests/settings/billing/billing-settings.spec.ts b/frontend/e2e/tests/settings/billing/billing-settings.spec.ts
new file mode 100644
index 00000000000..b8805426d2d
--- /dev/null
+++ b/frontend/e2e/tests/settings/billing/billing-settings.spec.ts
@@ -0,0 +1,71 @@
+import { expect, test } from '@playwright/test';
+
+import { ensureLoggedIn } from '../../../utils/login.util';
+
+// E2E: Billing Settings - View Billing Information and Button Actions
+
+test('View Billing Information and Button Actions', async ({
+ page,
+ context,
+}) => {
+ // Ensure user is logged in
+ await ensureLoggedIn(page);
+
+ // 1. Open the sidebar settings menu using data-testid
+ await page.getByTestId('settings-nav-item').click();
+
+ // 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
+ await page.getByRole('menuitem', { name: 'Account Settings' }).click();
+
+ // Assert the main tabpanel/heading (confirmed by DOM)
+ await expect(page.getByTestId('settings-page-title')).toBeVisible();
+
+ // Focus on the settings page sidenav
+ await page.getByTestId('settings-page-sidenav').focus();
+
+ // Click Billing tab in the settings sidebar (by data-testid)
+ await page.getByTestId('billing').click();
+
+ // Wait for billing chart/data to finish loading
+ await page.getByText('loading').first().waitFor({ state: 'hidden' });
+
+ // Assert visibility of subheading (unique)
+ await expect(
+ page.getByText(
+ 'Manage your billing information, invoices, and monitor costs.',
+ ),
+ ).toBeVisible();
+ // Assert visibility of Teams Cloud heading
+ await expect(page.getByRole('heading', { name: 'Teams Cloud' })).toBeVisible();
+
+ // Assert presence of summary and detailed tables
+ await expect(page.getByText('TOTAL SPENT')).toBeVisible();
+ await expect(page.getByText('Data Ingested')).toBeVisible();
+ await expect(page.getByText('Price per Unit')).toBeVisible();
+ await expect(page.getByText('Cost (Billing period to date)')).toBeVisible();
+
+ // Assert presence of alert and note
+ await expect(
+ page.getByText('Your current billing period is from', { exact: false }),
+ ).toBeVisible();
+ await expect(
+ page.getByText('Billing metrics are updated once every 24 hours.'),
+ ).toBeVisible();
+
+ // Test Download CSV button
+ const [download] = await Promise.all([
+ page.waitForEvent('download'),
+ page.getByRole('button', { name: 'cloud-download Download CSV' }).click(),
+ ]);
+ // Optionally, check download file name
+ expect(download.suggestedFilename()).toContain('billing_usage');
+
+ // Test Manage Billing button (opens Stripe in new tab)
+ const [newPage] = await Promise.all([
+ context.waitForEvent('page'),
+ page.getByTestId('header-billing-button').click(),
+ ]);
+ await newPage.waitForLoadState();
+ expect(newPage.url()).toContain('stripe.com');
+ await newPage.close();
+});
diff --git a/frontend/e2e/tests/settings/custom-domain/custom-domain-settings.spec.ts b/frontend/e2e/tests/settings/custom-domain/custom-domain-settings.spec.ts
new file mode 100644
index 00000000000..19a0247b453
--- /dev/null
+++ b/frontend/e2e/tests/settings/custom-domain/custom-domain-settings.spec.ts
@@ -0,0 +1,52 @@
+import { expect, test } from '@playwright/test';
+
+import { ensureLoggedIn } from '../../../utils/login.util';
+
+test('Custom Domain Settings - View and Interact', async ({ page }) => {
+ await ensureLoggedIn(page);
+
+ // 1. Open the sidebar settings menu using data-testid
+ await page.getByTestId('settings-nav-item').click();
+
+ // 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
+ await page.getByRole('menuitem', { name: 'Account Settings' }).click();
+
+ // Assert the main tabpanel/heading (confirmed by DOM)
+ await expect(page.getByTestId('settings-page-title')).toBeVisible();
+
+ // Focus on the settings page sidenav
+ await page.getByTestId('settings-page-sidenav').focus();
+
+ // Click Custom Domain tab in the settings sidebar (by data-testid)
+ await page.getByTestId('custom-domain').click();
+
+ // Wait for custom domain chart/data to finish loading
+ await page.getByText('loading').first().waitFor({ state: 'hidden' });
+
+ // Assert heading and subheading
+ await expect(
+ page.getByRole('heading', { name: 'Custom Domain Settings' }),
+ ).toBeVisible();
+ await expect(
+ page.getByText('Personalize your workspace domain effortlessly.'),
+ ).toBeVisible();
+
+ // Assert presence of Customize team’s URL button
+ const customizeBtn = page.getByRole('button', {
+ name: 'Customize team’s URL',
+ });
+ await expect(customizeBtn).toBeVisible();
+ await customizeBtn.click();
+
+ // Assert modal/dialog fields and buttons
+ await expect(
+ page.getByRole('dialog', { name: 'Customize your team’s URL' }),
+ ).toBeVisible();
+ await expect(page.getByLabel('Team’s URL subdomain')).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: 'Apply Changes' }),
+ ).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Close' })).toBeVisible();
+ // Close the modal
+ await page.getByRole('button', { name: 'Close' }).click();
+});
diff --git a/frontend/e2e/tests/settings/general/general-settings.spec.ts b/frontend/e2e/tests/settings/general/general-settings.spec.ts
new file mode 100644
index 00000000000..346659fe3b3
--- /dev/null
+++ b/frontend/e2e/tests/settings/general/general-settings.spec.ts
@@ -0,0 +1,32 @@
+import { expect, test } from '@playwright/test';
+
+import { ensureLoggedIn } from '../../../utils/login.util';
+
+test('View General Settings', async ({ page }) => {
+ await ensureLoggedIn(page);
+
+ // 1. Open the sidebar settings menu using data-testid
+ await page.getByTestId('settings-nav-item').click();
+
+ // 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
+ await page.getByRole('menuitem', { name: 'Account Settings' }).click();
+
+ // Assert the main tabpanel/heading (confirmed by DOM)
+ await expect(page.getByTestId('settings-page-title')).toBeVisible();
+
+ // Focus on the settings page sidenav
+ await page.getByTestId('settings-page-sidenav').focus();
+
+ // Click General tab in the settings sidebar (by data-testid)
+ await page.getByTestId('general').click();
+
+ // Wait for General tab to be visible
+ await page.getByRole('tabpanel', { name: 'General' }).waitFor();
+
+ // Assert visibility of definitive/static elements
+ await expect(page.getByRole('heading', { name: 'Metrics' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Traces' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Logs' })).toBeVisible();
+ await expect(page.getByText('Please')).toBeVisible();
+ await expect(page.getByRole('link', { name: 'email us' })).toBeVisible();
+});
diff --git a/frontend/e2e/tests/settings/ingestion/ingestion-settings.spec.ts b/frontend/e2e/tests/settings/ingestion/ingestion-settings.spec.ts
new file mode 100644
index 00000000000..c18fee0c804
--- /dev/null
+++ b/frontend/e2e/tests/settings/ingestion/ingestion-settings.spec.ts
@@ -0,0 +1,48 @@
+import { expect, test } from '@playwright/test';
+
+import { ensureLoggedIn } from '../../../utils/login.util';
+
+test('Ingestion Settings - View and Interact', async ({ page }) => {
+ await ensureLoggedIn(page);
+
+ // 1. Open the sidebar settings menu using data-testid
+ await page.getByTestId('settings-nav-item').click();
+
+ // 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
+ await page.getByRole('menuitem', { name: 'Account Settings' }).click();
+
+ // Assert the main tabpanel/heading (confirmed by DOM)
+ await expect(page.getByTestId('settings-page-title')).toBeVisible();
+
+ // Focus on the settings page sidenav
+ await page.getByTestId('settings-page-sidenav').focus();
+
+ // Click Ingestion tab in the settings sidebar (by data-testid)
+ await page.getByTestId('ingestion').click();
+
+ // Assert heading and subheading (Integrations page)
+ await expect(
+ page.getByRole('heading', { name: 'Integrations' }),
+ ).toBeVisible();
+ await expect(
+ page.getByText('Manage Integrations for this workspace'),
+ ).toBeVisible();
+
+ // Assert presence of search box
+ await expect(
+ page.getByPlaceholder('Search for an integration...'),
+ ).toBeVisible();
+
+ // Assert at least one data source with Configure button
+ const configureBtn = page.getByRole('button', { name: 'Configure' }).first();
+ await expect(configureBtn).toBeVisible();
+
+ // Assert Request more integrations section
+ await expect(
+ page.getByText(
+ "Can't find what you’re looking for? Request more integrations",
+ ),
+ ).toBeVisible();
+ await expect(page.getByPlaceholder('Enter integration name...')).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
+});
diff --git a/frontend/e2e/tests/settings/integrations/integrations-settings.spec.ts b/frontend/e2e/tests/settings/integrations/integrations-settings.spec.ts
new file mode 100644
index 00000000000..3c55614cec5
--- /dev/null
+++ b/frontend/e2e/tests/settings/integrations/integrations-settings.spec.ts
@@ -0,0 +1,48 @@
+import { expect, test } from '@playwright/test';
+
+import { ensureLoggedIn } from '../../../utils/login.util';
+
+test('Integrations Settings - View and Interact', async ({ page }) => {
+ await ensureLoggedIn(page);
+
+ // 1. Open the sidebar settings menu using data-testid
+ await page.getByTestId('settings-nav-item').click();
+
+ // 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
+ await page.getByRole('menuitem', { name: 'Account Settings' }).click();
+
+ // Assert the main tabpanel/heading (confirmed by DOM)
+ await expect(page.getByTestId('settings-page-title')).toBeVisible();
+
+ // Focus on the settings page sidenav
+ await page.getByTestId('settings-page-sidenav').focus();
+
+ // Click Integrations tab in the settings sidebar (by data-testid)
+ await page.getByTestId('integrations').click();
+
+ // Assert heading and subheading
+ await expect(
+ page.getByRole('heading', { name: 'Integrations' }),
+ ).toBeVisible();
+ await expect(
+ page.getByText('Manage Integrations for this workspace'),
+ ).toBeVisible();
+
+ // Assert presence of search box
+ await expect(
+ page.getByPlaceholder('Search for an integration...'),
+ ).toBeVisible();
+
+ // Assert at least one integration with Configure button
+ const configureBtn = page.getByRole('button', { name: 'Configure' }).first();
+ await expect(configureBtn).toBeVisible();
+
+ // Assert Request more integrations section
+ await expect(
+ page.getByText(
+ "Can't find what you’re looking for? Request more integrations",
+ ),
+ ).toBeVisible();
+ await expect(page.getByPlaceholder('Enter integration name...')).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
+});
diff --git a/frontend/e2e/tests/settings/members-sso/members-sso-settings.spec.ts b/frontend/e2e/tests/settings/members-sso/members-sso-settings.spec.ts
new file mode 100644
index 00000000000..02d9880fbfe
--- /dev/null
+++ b/frontend/e2e/tests/settings/members-sso/members-sso-settings.spec.ts
@@ -0,0 +1,56 @@
+import { expect, test } from '@playwright/test';
+
+import { ensureLoggedIn } from '../../../utils/login.util';
+
+test('Members & SSO Settings - View and Interact', async ({ page }) => {
+ await ensureLoggedIn(page);
+
+ // 1. Open the sidebar settings menu using data-testid
+ await page.getByTestId('settings-nav-item').click();
+
+ // 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
+ await page.getByRole('menuitem', { name: 'Account Settings' }).click();
+
+ // Assert the main tabpanel/heading (confirmed by DOM)
+ await expect(page.getByTestId('settings-page-title')).toBeVisible();
+
+ // Focus on the settings page sidenav
+ await page.getByTestId('settings-page-sidenav').focus();
+
+ // Click Members & SSO tab in the settings sidebar (by data-testid)
+ await page.getByTestId('members-sso').click();
+
+ // Assert headings and tables
+ await expect(
+ page.getByRole('heading', { name: /Members \(\d+\)/ }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole('heading', { name: /Pending Invites \(\d+\)/ }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole('heading', { name: 'Authenticated Domains' }),
+ ).toBeVisible();
+
+ // Assert Invite Members button is visible and clickable
+ const inviteBtn = page.getByRole('button', { name: /Invite Members/ });
+ await expect(inviteBtn).toBeVisible();
+ await inviteBtn.click();
+ // Assert Invite Members modal/dialog appears (modal title is unique)
+ await expect(page.getByText('Invite team members').first()).toBeVisible();
+ // Close the modal (use unique 'Close' button)
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Assert Edit and Delete buttons are present for at least one member
+ const editBtn = page.getByRole('button', { name: /Edit/ }).first();
+ const deleteBtn = page.getByRole('button', { name: /Delete/ }).first();
+ await expect(editBtn).toBeVisible();
+ await expect(deleteBtn).toBeVisible();
+
+ // Assert Add Domains button is visible
+ await expect(page.getByRole('button', { name: /Add Domains/ })).toBeVisible();
+ // Assert Configure SSO or Edit Google Auth button is visible for at least one domain
+ const ssoBtn = page
+ .getByRole('button', { name: /Configure SSO|Edit Google Auth/ })
+ .first();
+ await expect(ssoBtn).toBeVisible();
+});
diff --git a/frontend/e2e/tests/settings/notification-channels/notification-channels-settings.spec.ts b/frontend/e2e/tests/settings/notification-channels/notification-channels-settings.spec.ts
new file mode 100644
index 00000000000..432597671f1
--- /dev/null
+++ b/frontend/e2e/tests/settings/notification-channels/notification-channels-settings.spec.ts
@@ -0,0 +1,57 @@
+import { expect, test } from '@playwright/test';
+
+import { ensureLoggedIn } from '../../../utils/login.util';
+
+test('Notification Channels Settings - View and Interact', async ({ page }) => {
+ await ensureLoggedIn(page);
+
+ // 1. Open the sidebar settings menu using data-testid
+ await page.getByTestId('settings-nav-item').click();
+
+ // 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
+ await page.getByRole('menuitem', { name: 'Account Settings' }).click();
+
+ // Assert the main tabpanel/heading (confirmed by DOM)
+ await expect(page.getByTestId('settings-page-title')).toBeVisible();
+
+ // Focus on the settings page sidenav
+ await page.getByTestId('settings-page-sidenav').focus();
+
+ // Click Notification Channels tab in the settings sidebar (by data-testid)
+ await page.getByTestId('notification-channels').click();
+
+ // Wait for loading to finish
+ await page.getByText('loading').first().waitFor({ state: 'hidden' });
+
+ // Assert presence of New Alert Channel button
+ const newChannelBtn = page.getByRole('button', { name: /New Alert Channel/ });
+ await expect(newChannelBtn).toBeVisible();
+
+ // Assert table columns
+ await expect(page.getByText('Name')).toBeVisible();
+ await expect(page.getByText('Type')).toBeVisible();
+ await expect(page.getByText('Action')).toBeVisible();
+
+ // Click New Alert Channel and assert modal fields/buttons
+ await newChannelBtn.click();
+ await expect(
+ page.getByRole('heading', { name: 'New Notification Channel' }),
+ ).toBeVisible();
+ await expect(page.getByLabel('Name')).toBeVisible();
+ await expect(page.getByLabel('Type')).toBeVisible();
+ await expect(page.getByLabel('Webhook URL')).toBeVisible();
+ await expect(
+ page.getByRole('switch', { name: 'Send resolved alerts' }),
+ ).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Test' })).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Back' })).toBeVisible();
+ // Close modal
+ await page.getByRole('button', { name: 'Back' }).click();
+
+ // Assert Edit and Delete buttons for at least one channel
+ const editBtn = page.getByRole('button', { name: 'Edit' }).first();
+ const deleteBtn = page.getByRole('button', { name: 'Delete' }).first();
+ await expect(editBtn).toBeVisible();
+ await expect(deleteBtn).toBeVisible();
+});
diff --git a/frontend/e2e/utils/login.util.ts b/frontend/e2e/utils/login.util.ts
new file mode 100644
index 00000000000..c425e31e72e
--- /dev/null
+++ b/frontend/e2e/utils/login.util.ts
@@ -0,0 +1,35 @@
+import { Page } from '@playwright/test';
+
+// Read credentials from environment variables
+const username = process.env.LOGIN_USERNAME;
+const password = process.env.LOGIN_PASSWORD;
+const baseURL = process.env.BASE_URL;
+
+/**
+ * Ensures the user is logged in. If not, performs the login steps.
+ * Follows the MCP process step-by-step.
+ */
+export async function ensureLoggedIn(page: Page): Promise {
+ // if already in home page, return
+ if (await page.url().includes('/home')) {
+ return;
+ }
+
+ if (!username || !password) {
+ throw new Error(
+ 'E2E_EMAIL and E2E_PASSWORD environment variables must be set.',
+ );
+ }
+
+ await page.goto(`${baseURL}/login`);
+ await page.getByTestId('email').click();
+ await page.getByTestId('email').fill(username);
+ await page.getByTestId('initiate_login').click();
+ await page.getByTestId('password').click();
+ await page.getByTestId('password').fill(password);
+ await page.getByRole('button', { name: 'Login' }).click();
+
+ await page
+ .getByText('Hello there, Welcome to your')
+ .waitFor({ state: 'visible' });
+}
diff --git a/frontend/example.env b/frontend/example.env
index 5e1271b1578..d875044f96e 100644
--- a/frontend/example.env
+++ b/frontend/example.env
@@ -1,6 +1,7 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
FRONTEND_API_ENDPOINT="http://localhost:8080/"
-INTERCOM_APP_ID="intercom-app-id"
+PYLON_APP_ID="pylon-app-id"
+APPCUES_APP_ID="appcess-app-id"
CI="1"
\ No newline at end of file
diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts
index c16339d5177..a5e8d86c3fc 100644
--- a/frontend/jest.config.ts
+++ b/frontend/jest.config.ts
@@ -1,6 +1,9 @@
import type { Config } from '@jest/types';
+const USE_SAFE_NAVIGATE_MOCK_PATH = '/__mocks__/useSafeNavigate.ts';
+
const config: Config.InitialOptions = {
+ silent: true,
clearMocks: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'cobertura', 'html', 'json-summary'],
@@ -10,11 +13,17 @@ const config: Config.InitialOptions = {
moduleNameMapper: {
'\\.(css|less|scss)$': '/__mocks__/cssMock.ts',
'\\.md$': '/__mocks__/cssMock.ts',
+ '^uplot$': '/__mocks__/uplotMock.ts',
+ '^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
+ '^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
+ '^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
},
globals: {
extensionsToTreatAsEsm: ['.ts'],
'ts-jest': {
useESM: true,
+ isolatedModules: true,
+ tsconfig: '/tsconfig.jest.json',
},
},
testMatch: ['/src/**/*?(*.)(test).(ts|js)?(x)'],
@@ -24,7 +33,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
- 'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api)/)',
+ 'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
],
setupFilesAfterEnv: ['jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],
diff --git a/frontend/package.json b/frontend/package.json
index 72b14931383..6316f619891 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -28,6 +28,8 @@
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
+ "@codemirror/autocomplete": "6.18.6",
+ "@codemirror/lang-javascript": "6.2.3",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
@@ -36,13 +38,27 @@
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
+ "@playwright/test": "1.54.1",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6",
+ "@signozhq/badge": "0.0.2",
+ "@signozhq/button": "0.0.2",
+ "@signozhq/calendar": "0.0.0",
+ "@signozhq/callout": "0.0.2",
"@signozhq/design-tokens": "1.1.4",
+ "@signozhq/input": "0.0.2",
+ "@signozhq/popover": "0.0.0",
+ "@signozhq/resizable": "0.0.0",
+ "@signozhq/sonner": "0.1.0",
+ "@signozhq/table": "0.3.7",
+ "@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
+ "@uiw/codemirror-theme-copilot": "4.23.11",
+ "@uiw/codemirror-theme-github": "4.24.1",
+ "@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/hierarchy": "3.12.0",
@@ -52,6 +68,7 @@
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
+ "antlr4": "4.13.2",
"axios": "1.8.2",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
@@ -78,13 +95,14 @@
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"html-webpack-plugin": "5.5.0",
- "http-proxy-middleware": "3.0.3",
+ "http-proxy-middleware": "3.0.5",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"jest": "^27.5.1",
"js-base64": "^3.7.2",
+ "kbar": "0.1.0-beta.48",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
@@ -121,6 +139,7 @@
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
+ "rrule": "2.8.1",
"stream": "^0.0.2",
"style-loader": "1.3.0",
"styled-components": "^5.3.11",
@@ -134,7 +153,7 @@
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
- "webpack-dev-server": "^4.15.2",
+ "webpack-dev-server": "^5.2.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
},
@@ -197,7 +216,6 @@
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^11.0.0",
- "critters-webpack-plugin": "^3.0.1",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.4",
@@ -214,7 +232,9 @@
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
- "image-webpack-loader": "8.1.0",
+ "image-minimizer-webpack-plugin": "^4.0.0",
+ "imagemin": "^8.0.1",
+ "imagemin-svgo": "^10.0.1",
"is-ci": "^3.0.1",
"jest-styled-components": "^7.0.8",
"lint-staged": "^12.5.0",
@@ -231,11 +251,12 @@
"redux-mock-store": "1.5.4",
"sass": "1.66.1",
"sass-loader": "13.3.2",
+ "sharp": "^0.33.4",
"ts-jest": "^27.1.5",
"ts-node": "^10.2.1",
- "typescript-plugin-css-modules": "5.0.1",
+ "typescript-plugin-css-modules": "5.2.0",
"webpack-bundle-analyzer": "^4.5.0",
- "webpack-cli": "^4.9.2"
+ "webpack-cli": "^5.1.4"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
@@ -251,10 +272,13 @@
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
- "http-proxy-middleware": "3.0.3",
+ "http-proxy-middleware": "3.0.5",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
- "prismjs": "1.30.0"
+ "prismjs": "1.30.0",
+ "got": "11.8.5",
+ "form-data": "4.0.4",
+ "brace-expansion": "^2.0.2"
}
}
diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts
new file mode 100644
index 00000000000..88ee733baa1
--- /dev/null
+++ b/frontend/playwright.config.ts
@@ -0,0 +1,95 @@
+import { defineConfig, devices } from '@playwright/test';
+import dotenv from 'dotenv';
+import path from 'path';
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// import dotenv from 'dotenv';
+// import path from 'path';
+// dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './e2e/tests',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Run tests in parallel even in CI - optimized for GitHub Actions free tier */
+ workers: process.env.CI ? 2 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL:
+ process.env.SIGNOZ_E2E_BASE_URL || 'https://app.us.staging.signoz.cloud',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ colorScheme: 'dark',
+ locale: 'en-US',
+ viewport: { width: 1280, height: 720 },
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ launchOptions: { args: ['--start-maximized'] },
+ viewport: null,
+ colorScheme: 'dark',
+ locale: 'en-US',
+ baseURL: 'https://app.us.staging.signoz.cloud',
+ trace: 'on-first-retry',
+ },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: { ...devices['Pixel 5'] },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: { ...devices['iPhone 12'] },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ // },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ // webServer: {
+ // command: 'npm run start',
+ // url: 'http://localhost:3000',
+ // reuseExistingServer: !process.env.CI,
+ // },
+});
diff --git a/frontend/prompts/generate-e2e-test.md b/frontend/prompts/generate-e2e-test.md
new file mode 100644
index 00000000000..29caf5ab61b
--- /dev/null
+++ b/frontend/prompts/generate-e2e-test.md
@@ -0,0 +1,16 @@
+RULE: All test code for this repo must be generated by following the step-by-step Playwright MCP process as described below.
+
+- You are a playwright test generator.
+- You are given a scenario and you need to generate a playwright test for it.
+- Use login util if not logged in.
+- DO NOT generate test code based on the scenario alone.
+- DO run steps one by one using the tools provided by the Playwright MCP.
+- Only after all steps are completed, emit a Playwright TypeScript test that uses @playwright/test based on message history
+- Gather correct selectors before writing the test
+- DO NOT valiate for dynamic content in the tests, only validate for the correctness with meta data
+- Always inspect the DOM at each navigation or interaction step to determine the correct selector for the next action. Do not assume selectors, confirm via inspection before proceeding.
+- Assert visibility of definitive/static elements in the UI (such as labels, headings, or section titles) rather than dynamic values or content that may change between runs.
+- Save generated test file in the tests directory
+- Execute the test file and iterate until the test passes
+
+
diff --git a/frontend/public/Images/cloud.svg b/frontend/public/Images/cloud.svg
new file mode 100644
index 00000000000..c7138d589b2
--- /dev/null
+++ b/frontend/public/Images/cloud.svg
@@ -0,0 +1,81 @@
+
+
\ No newline at end of file
diff --git a/frontend/public/Logos/argocd.svg b/frontend/public/Logos/argocd.svg
new file mode 100644
index 00000000000..ef6eff54507
--- /dev/null
+++ b/frontend/public/Logos/argocd.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/public/Logos/azure-mysql.svg b/frontend/public/Logos/azure-mysql.svg
new file mode 100644
index 00000000000..d1504cc763f
--- /dev/null
+++ b/frontend/public/Logos/azure-mysql.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/Logos/cloudflare.svg b/frontend/public/Logos/cloudflare.svg
new file mode 100644
index 00000000000..84ce0dc0165
--- /dev/null
+++ b/frontend/public/Logos/cloudflare.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/Logos/dynamodb.svg b/frontend/public/Logos/dynamodb.svg
new file mode 100644
index 00000000000..bd4f2c30f50
--- /dev/null
+++ b/frontend/public/Logos/dynamodb.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/frontend/public/Logos/elk.svg b/frontend/public/Logos/elk.svg
new file mode 100644
index 00000000000..d240ad568c8
--- /dev/null
+++ b/frontend/public/Logos/elk.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/frontend/public/Logos/external-api-monitoring.svg b/frontend/public/Logos/external-api-monitoring.svg
new file mode 100644
index 00000000000..327eaa95064
--- /dev/null
+++ b/frontend/public/Logos/external-api-monitoring.svg
@@ -0,0 +1,19 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/public/Logos/github-actions.svg b/frontend/public/Logos/github-actions.svg
new file mode 100644
index 00000000000..2929e024f39
--- /dev/null
+++ b/frontend/public/Logos/github-actions.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/public/Logos/github.svg b/frontend/public/Logos/github.svg
new file mode 100644
index 00000000000..3ff1cee7c9b
--- /dev/null
+++ b/frontend/public/Logos/github.svg
@@ -0,0 +1,5 @@
+
diff --git a/frontend/public/Logos/http-monitoring.svg b/frontend/public/Logos/http-monitoring.svg
new file mode 100644
index 00000000000..b495ddf1989
--- /dev/null
+++ b/frontend/public/Logos/http-monitoring.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/Logos/jenkins.svg b/frontend/public/Logos/jenkins.svg
new file mode 100644
index 00000000000..c4e7b880d36
--- /dev/null
+++ b/frontend/public/Logos/jenkins.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/Logos/newrelic.svg b/frontend/public/Logos/newrelic.svg
new file mode 100644
index 00000000000..e2d586bf718
--- /dev/null
+++ b/frontend/public/Logos/newrelic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/Logos/openai.svg b/frontend/public/Logos/openai.svg
new file mode 100644
index 00000000000..94eb50385dc
--- /dev/null
+++ b/frontend/public/Logos/openai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/Logos/s3.svg b/frontend/public/Logos/s3.svg
new file mode 100644
index 00000000000..cd203eaad6e
--- /dev/null
+++ b/frontend/public/Logos/s3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/Logos/snowflake.svg b/frontend/public/Logos/snowflake.svg
new file mode 100644
index 00000000000..f491c273133
--- /dev/null
+++ b/frontend/public/Logos/snowflake.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/Logos/sns.svg b/frontend/public/Logos/sns.svg
new file mode 100644
index 00000000000..6cb54adab20
--- /dev/null
+++ b/frontend/public/Logos/sns.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/frontend/public/Logos/sqs.svg b/frontend/public/Logos/sqs.svg
new file mode 100644
index 00000000000..b19102943cd
--- /dev/null
+++ b/frontend/public/Logos/sqs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/Logos/systemd.svg b/frontend/public/Logos/systemd.svg
new file mode 100644
index 00000000000..4a9d9492ff7
--- /dev/null
+++ b/frontend/public/Logos/systemd.svg
@@ -0,0 +1,8 @@
+
diff --git a/frontend/public/Logos/wordpress.svg b/frontend/public/Logos/wordpress.svg
new file mode 100644
index 00000000000..916903f23b1
--- /dev/null
+++ b/frontend/public/Logos/wordpress.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/locales/en-GB/alerts.json b/frontend/public/locales/en-GB/alerts.json
index 8dd0ccbe47a..8219da73873 100644
--- a/frontend/public/locales/en-GB/alerts.json
+++ b/frontend/public/locales/en-GB/alerts.json
@@ -14,8 +14,8 @@
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
- "alert_form_step2": "Step 2 - Define Alert Conditions",
- "alert_form_step3": "Step 3 - Alert Configuration",
+ "alert_form_step2": "Step {{step}} - Define Alert Conditions",
+ "alert_form_step3": "Step {{step}} - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
diff --git a/frontend/public/locales/en-GB/channels.json b/frontend/public/locales/en-GB/channels.json
index 6ce4b6f2c15..4c1980fe9d4 100644
--- a/frontend/public/locales/en-GB/channels.json
+++ b/frontend/public/locales/en-GB/channels.json
@@ -9,8 +9,8 @@
"tooltip_notification_channels": "More details on how to setting notification channels",
"sending_channels_note": "The alerts will be sent to all the configured channels.",
"loading_channels_message": "Loading Channels..",
- "page_title_create": "New Notification Channels",
- "page_title_edit": "Edit Notification Channels",
+ "page_title_create": "New Notification Channel",
+ "page_title_edit": "Edit Notification Channel",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",
@@ -62,5 +62,8 @@
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
"webhook_url_required": "Webhook URL is mandatory",
- "slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
+ "slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
+ "api_key_required": "API Key is mandatory",
+ "to_required": "To field is mandatory",
+ "channel_name_required": "Channel name is mandatory"
}
\ No newline at end of file
diff --git a/frontend/public/locales/en-GB/failedPayment.json b/frontend/public/locales/en-GB/failedPayment.json
index a624e47c7db..05ec0cc68d4 100644
--- a/frontend/public/locales/en-GB/failedPayment.json
+++ b/frontend/public/locales/en-GB/failedPayment.json
@@ -8,5 +8,6 @@
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Settle your bill to continue",
- "somethingWentWrong": "Something went wrong"
+ "somethingWentWrong": "Something went wrong",
+ "refreshPaymentStatus": "Refresh Status"
}
diff --git a/frontend/public/locales/en-GB/rules.json b/frontend/public/locales/en-GB/rules.json
index 63ae437d7ff..edb6d6f3ef5 100644
--- a/frontend/public/locales/en-GB/rules.json
+++ b/frontend/public/locales/en-GB/rules.json
@@ -7,8 +7,8 @@
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
- "alert_form_step2": "Step 2 - Define Alert Conditions",
- "alert_form_step3": "Step 3 - Alert Configuration",
+ "alert_form_step2": "Step {{step}} - Define Alert Conditions",
+ "alert_form_step3": "Step {{step}} - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
diff --git a/frontend/public/locales/en-GB/services.json b/frontend/public/locales/en-GB/services.json
index f04c8517599..a8c8b42c9c7 100644
--- a/frontend/public/locales/en-GB/services.json
+++ b/frontend/public/locales/en-GB/services.json
@@ -1,3 +1,3 @@
{
- "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support or "
+ "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via chat support or "
}
diff --git a/frontend/public/locales/en-GB/titles.json b/frontend/public/locales/en-GB/titles.json
index 95a58e615f9..f5ce505de3c 100644
--- a/frontend/public/locales/en-GB/titles.json
+++ b/frontend/public/locales/en-GB/titles.json
@@ -46,5 +46,8 @@
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
- "INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
+ "INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
+ "METER_EXPLORER": "SigNoz | Meter Explorer",
+ "METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
+ "METER": "SigNoz | Meter"
}
diff --git a/frontend/public/locales/en/alerts.json b/frontend/public/locales/en/alerts.json
index 6adeb7382bc..e49ab3bc7c3 100644
--- a/frontend/public/locales/en/alerts.json
+++ b/frontend/public/locales/en/alerts.json
@@ -129,5 +129,6 @@
"text_num_points": "data points in each result group",
"text_alert_frequency": "Run alert every",
"text_for": "minutes",
- "selected_query_placeholder": "Select query"
+ "selected_query_placeholder": "Select query",
+ "alert_rule_not_found": "Alert Rule not found"
}
diff --git a/frontend/public/locales/en/channels.json b/frontend/public/locales/en/channels.json
index 1cbc4cf46db..5068e125f7e 100644
--- a/frontend/public/locales/en/channels.json
+++ b/frontend/public/locales/en/channels.json
@@ -9,8 +9,8 @@
"tooltip_notification_channels": "More details on how to setting notification channels",
"sending_channels_note": "The alerts will be sent to all the configured channels.",
"loading_channels_message": "Loading Channels..",
- "page_title_create": "New Notification Channels",
- "page_title_edit": "Edit Notification Channels",
+ "page_title_create": "New Notification Channel",
+ "page_title_edit": "Edit Notification Channel",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",
@@ -77,5 +77,8 @@
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
"webhook_url_required": "Webhook URL is mandatory",
- "slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
+ "slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
+ "api_key_required": "API Key is mandatory",
+ "to_required": "To field is mandatory",
+ "channel_name_required": "Channel name is mandatory"
}
\ No newline at end of file
diff --git a/frontend/public/locales/en/failedPayment.json b/frontend/public/locales/en/failedPayment.json
index a624e47c7db..05ec0cc68d4 100644
--- a/frontend/public/locales/en/failedPayment.json
+++ b/frontend/public/locales/en/failedPayment.json
@@ -8,5 +8,6 @@
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Settle your bill to continue",
- "somethingWentWrong": "Something went wrong"
+ "somethingWentWrong": "Something went wrong",
+ "refreshPaymentStatus": "Refresh Status"
}
diff --git a/frontend/public/locales/en/rules.json b/frontend/public/locales/en/rules.json
index 63ae437d7ff..edb6d6f3ef5 100644
--- a/frontend/public/locales/en/rules.json
+++ b/frontend/public/locales/en/rules.json
@@ -7,8 +7,8 @@
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
- "alert_form_step2": "Step 2 - Define Alert Conditions",
- "alert_form_step3": "Step 3 - Alert Configuration",
+ "alert_form_step2": "Step {{step}} - Define Alert Conditions",
+ "alert_form_step3": "Step {{step}} - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
diff --git a/frontend/public/locales/en/services.json b/frontend/public/locales/en/services.json
index f04c8517599..a8c8b42c9c7 100644
--- a/frontend/public/locales/en/services.json
+++ b/frontend/public/locales/en/services.json
@@ -1,3 +1,3 @@
{
- "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support or "
+ "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via chat support or "
}
diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json
index 9e4039a86b7..3ab532a9138 100644
--- a/frontend/public/locales/en/titles.json
+++ b/frontend/public/locales/en/titles.json
@@ -69,5 +69,8 @@
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
- "API_MONITORING": "SigNoz | External APIs"
+ "API_MONITORING": "SigNoz | External APIs",
+ "METER_EXPLORER": "SigNoz | Meter Explorer",
+ "METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
+ "METER": "SigNoz | Meter"
}
diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx
index 092fedaae6a..8d80e0bbbd7 100644
--- a/frontend/src/AppRoutes/Private.tsx
+++ b/frontend/src/AppRoutes/Private.tsx
@@ -3,6 +3,7 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
import getAll from 'api/v1/user/get';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
+import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
@@ -14,6 +15,7 @@ import { matchPath, useLocation } from 'react-router-dom';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
+import { OrgPreference } from 'types/api/preferences/preference';
import { Organization } from 'types/api/user/getOrganization';
import { UserResponse } from 'types/api/user/getUser';
import { USER_ROLES } from 'types/roles';
@@ -36,8 +38,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
user,
isLoggedIn: isLoggedInState,
isFetchingOrgPreferences,
- activeLicenseV3,
- isFetchingActiveLicenseV3,
+ activeLicense,
+ isFetchingActiveLicense,
trialInfo,
featureFlags,
} = useAppContext();
@@ -78,7 +80,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const checkFirstTimeUser = useCallback((): boolean => {
const users = usersData?.data || [];
- const remainingUsers = users.filter(
+ const remainingUsers = (Array.isArray(users) ? users : []).filter(
(user) => user.email !== 'admin@signoz.cloud',
);
@@ -95,7 +97,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
usersData.data
) {
const isOnboardingComplete = orgPreferences?.find(
- (preference: Record) => preference.key === 'ORG_ONBOARDING',
+ (preference: OrgPreference) =>
+ preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
)?.value;
const isFirstUser = checkFirstTimeUser();
@@ -123,7 +126,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const isRouteEnabledForWorkspaceBlockedState =
isAdmin &&
- (path === ROUTES.ORG_SETTINGS ||
+ (path === ROUTES.SETTINGS ||
+ path === ROUTES.ORG_SETTINGS ||
path === ROUTES.BILLING ||
path === ROUTES.MY_SETTINGS);
@@ -145,16 +149,16 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
};
useEffect(() => {
- if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
+ if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
- const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED;
- const isExpired = activeLicenseV3.state === LicenseState.EXPIRED;
- const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED;
+ const isTerminated = activeLicense.state === LicenseState.TERMINATED;
+ const isExpired = activeLicense.state === LicenseState.EXPIRED;
+ const isCancelled = activeLicense.state === LicenseState.CANCELLED;
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
- const { platform } = activeLicenseV3;
+ const { platform } = activeLicense;
if (
isWorkspaceAccessRestricted &&
@@ -164,26 +168,26 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
navigateToWorkSpaceAccessRestricted(currentRoute);
}
}
- }, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]);
+ }, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
useEffect(() => {
- if (!isFetchingActiveLicenseV3) {
+ if (!isFetchingActiveLicense) {
const currentRoute = mapRoutes.get('current');
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
if (
shouldBlockWorkspace &&
currentRoute &&
- activeLicenseV3?.platform === LicensePlatform.CLOUD
+ activeLicense?.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceBlocked(currentRoute);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
- isFetchingActiveLicenseV3,
+ isFetchingActiveLicense,
trialInfo?.workSpaceBlock,
- activeLicenseV3?.platform,
+ activeLicense?.platform,
mapRoutes,
pathname,
]);
@@ -197,20 +201,20 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
};
useEffect(() => {
- if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
+ if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
- activeLicenseV3.state === LicenseState.DEFAULTED;
+ activeLicense.state === LicenseState.DEFAULTED;
if (
shouldSuspendWorkspace &&
currentRoute &&
- activeLicenseV3.platform === LicensePlatform.CLOUD
+ activeLicense.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceSuspended(currentRoute);
}
}
- }, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]);
+ }, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) {
diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx
index e24704a22cb..94ae9a90c7c 100644
--- a/frontend/src/AppRoutes/index.tsx
+++ b/frontend/src/AppRoutes/index.tsx
@@ -3,6 +3,8 @@ import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
+import AppLoading from 'components/AppLoading/AppLoading';
+import KBarCommandPalette from 'components/KBarCommandPalette/KBarCommandPalette';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
@@ -13,9 +15,9 @@ import AppLayout from 'container/AppLayout';
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useThemeConfig } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
-import { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute';
+import { StatusCodes } from 'http-status-codes';
import history from 'lib/history';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import posthog from 'posthog-js';
@@ -23,10 +25,13 @@ import AlertRuleProvider from 'providers/Alert';
import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
+import { ErrorModalProvider } from 'providers/ErrorModalProvider';
+import { KBarCommandPaletteProvider } from 'providers/KBarCommandPaletteProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
+import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { Userpilot } from 'userpilot';
import { extractDomain } from 'utils/app';
@@ -41,14 +46,13 @@ import defaultRoutes, {
function App(): JSX.Element {
const themeConfig = useThemeConfig();
const {
- licenses,
user,
isFetchingUser,
- isFetchingLicenses,
isFetchingFeatureFlags,
trialInfo,
- activeLicenseV3,
- isFetchingActiveLicenseV3,
+ activeLicense,
+ isFetchingActiveLicense,
+ activeLicenseFetchError,
userFetchError,
featureFlagsFetchError,
isLoggedIn: isLoggedInState,
@@ -66,11 +70,11 @@ function App(): JSX.Element {
const enableAnalytics = useCallback(
(user: IUser): void => {
// wait for the required data to be loaded before doing init for anything!
- if (!isFetchingActiveLicenseV3 && activeLicenseV3 && org) {
+ if (!isFetchingActiveLicense && activeLicense && org) {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
- const { displayName, email, role } = user;
+ const { displayName, email, role, id, orgId } = user;
const domain = extractDomain(email);
const hostNameParts = hostname.split('.');
@@ -103,6 +107,20 @@ function App(): JSX.Element {
if (domain) {
logEvent('Domain Identified', groupTraits, 'group');
}
+ if (window && window.Appcues) {
+ window.Appcues.identify(id, {
+ name: displayName,
+
+ tenant_id: hostNameParts[0],
+ data_region: hostNameParts[1],
+ tenant_url: hostname,
+ company_domain: domain,
+
+ companyName: orgName,
+ email,
+ paidUser: !!trialInfo?.trialConvertedToSubscription,
+ });
+ }
Userpilot.identify(email, {
email,
@@ -116,7 +134,7 @@ function App(): JSX.Element {
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
});
- posthog?.identify(email, {
+ posthog?.identify(id, {
email,
name: displayName,
orgName,
@@ -128,7 +146,7 @@ function App(): JSX.Element {
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
});
- posthog?.group('company', domain, {
+ posthog?.group('company', orgId, {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
@@ -137,24 +155,12 @@ function App(): JSX.Element {
source: 'signoz-ui',
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
});
-
- if (
- window.cioanalytics &&
- typeof window.cioanalytics.identify === 'function'
- ) {
- window.cioanalytics.reset();
- window.cioanalytics.identify(email, {
- name: user.displayName,
- email,
- role: user.role,
- });
- }
}
},
[
hostname,
- isFetchingActiveLicenseV3,
- activeLicenseV3,
+ isFetchingActiveLicense,
+ activeLicense,
org,
trialInfo?.trialConvertedToSubscription,
],
@@ -163,18 +169,19 @@ function App(): JSX.Element {
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
if (
- !isFetchingLicenses &&
- licenses &&
+ !isFetchingActiveLicense &&
+ (activeLicense || activeLicenseFetchError) &&
!isFetchingUser &&
user &&
!!user.email
) {
+ // either the active API returns error with 404 or 501 and if it returns a terminated license means it's on basic plan
const isOnBasicPlan =
- licenses.licenses?.some(
- (license) =>
- license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
- ) || licenses.licenses === null;
-
+ (activeLicenseFetchError &&
+ [StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
+ activeLicenseFetchError?.getHttpStatusCode(),
+ )) ||
+ (activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
@@ -187,15 +194,22 @@ function App(): JSX.Element {
// if the user is on basic plan then remove billing
if (isOnBasicPlan) {
updatedRoutes = updatedRoutes.filter(
- (route) => route?.path !== ROUTES.BILLING,
+ (route) =>
+ route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
);
}
+
+ if (isEnterpriseSelfHostedUser) {
+ updatedRoutes.push(LIST_LICENSES);
+ }
+
// always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
} else {
// if not a cloud user then remove billing and add list licenses route
updatedRoutes = updatedRoutes.filter(
- (route) => route?.path !== ROUTES.BILLING,
+ (route) =>
+ route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
);
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
}
@@ -204,22 +218,23 @@ function App(): JSX.Element {
}, [
isLoggedInState,
user,
- licenses,
isCloudUser,
isEnterpriseSelfHostedUser,
- isFetchingLicenses,
+ isFetchingActiveLicense,
isFetchingUser,
+ activeLicense,
+ activeLicenseFetchError,
]);
useEffect(() => {
if (pathname === ROUTES.ONBOARDING) {
- window.Intercom('update', {
- hide_default_launcher: true,
- });
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ window.Pylon('hideChatBubble');
} else {
- window.Intercom('update', {
- hide_default_launcher: false,
- });
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ window.Pylon('showChatBubble');
}
}, [pathname]);
@@ -231,8 +246,7 @@ function App(): JSX.Element {
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
- licenses &&
- activeLicenseV3 &&
+ activeLicense &&
trialInfo
) {
let isChatSupportEnabled = false;
@@ -255,11 +269,13 @@ function App(): JSX.Element {
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser)
) {
- window.Intercom('boot', {
- app_id: process.env.INTERCOM_APP_ID,
- email: user?.email || '',
- name: user?.displayName || '',
- });
+ window.pylon = {
+ chat_settings: {
+ app_id: process.env.PYLON_APP_ID,
+ email: user.email,
+ name: user.displayName,
+ },
+ };
}
}
}, [
@@ -270,8 +286,7 @@ function App(): JSX.Element {
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
- licenses,
- activeLicenseV3,
+ activeLicense,
trialInfo,
isCloudUser,
isEnterpriseSelfHostedUser,
@@ -322,10 +337,6 @@ function App(): JSX.Element {
} else {
posthog.reset();
Sentry.close();
-
- if (window.cioanalytics && typeof window.cioanalytics.reset === 'function') {
- window.cioanalytics.reset();
- }
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCloudUser, isEnterpriseSelfHostedUser]);
@@ -333,8 +344,8 @@ function App(): JSX.Element {
// if the user is in logged in state
if (isLoggedInState) {
// if the setup calls are loading then return a spinner
- if (isFetchingLicenses || isFetchingUser || isFetchingFeatureFlags) {
- return ;
+ if (isFetchingActiveLicense || isFetchingUser || isFetchingFeatureFlags) {
+ return ;
}
// if the required calls fails then return a something went wrong error
@@ -345,7 +356,11 @@ function App(): JSX.Element {
}
// if all of the data is not set then return a spinner, this is required because there is some gap between loading states and data setting
- if ((!licenses || !user.email || !featureFlags) && !userFetchError) {
+ if (
+ (!activeLicense || !user.email || !featureFlags) &&
+ !userFetchError &&
+ !activeLicenseFetchError
+ ) {
return ;
}
}
@@ -355,37 +370,42 @@ function App(): JSX.Element {
-
-
-
-
-
-
-
-
-
- }>
-
- {routes.map(({ path, component, exact }) => (
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ }>
+
+ {routes.map(({ path, component, exact }) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts
index 80a481ca330..c2833547f49 100644
--- a/frontend/src/AppRoutes/pageComponents.ts
+++ b/frontend/src/AppRoutes/pageComponents.ts
@@ -128,12 +128,7 @@ export const AlertOverview = Loadable(
);
export const CreateAlertChannelAlerts = Loadable(
- () =>
- import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
-);
-
-export const EditAlertChannelsAlerts = Loadable(
- () => import(/* webpackChunkName: "Edit Channels" */ 'pages/ChannelsEdit'),
+ () => import(/* webpackChunkName: "Create Channels" */ 'pages/Settings'),
);
export const AllAlertChannels = Loadable(
@@ -165,7 +160,7 @@ export const APIKeys = Loadable(
);
export const MySettings = Loadable(
- () => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
+ () => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
);
export const CustomDomainSettings = Loadable(
@@ -222,7 +217,7 @@ export const LogsIndexToFields = Loadable(
);
export const BillingPage = Loadable(
- () => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'),
+ () => import(/* webpackChunkName: "BillingPage" */ 'pages/Settings'),
);
export const SupportPage = Loadable(
@@ -249,7 +244,7 @@ export const WorkspaceAccessRestricted = Loadable(
);
export const ShortcutsPage = Loadable(
- () => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
+ () => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
);
export const InstalledIntegrations = Loadable(
diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts
index 5cf14278c2b..9553c1096ac 100644
--- a/frontend/src/AppRoutes/routes.ts
+++ b/frontend/src/AppRoutes/routes.ts
@@ -1,5 +1,6 @@
import ROUTES from 'constants/routes';
import MessagingQueues from 'pages/MessagingQueues';
+import MeterExplorer from 'pages/MeterExplorer';
import { RouteProps } from 'react-router-dom';
import {
@@ -7,20 +8,15 @@ import {
AlertOverview,
AllAlertChannels,
AllErrors,
- APIKeys,
ApiMonitoring,
- BillingPage,
CreateAlertChannelAlerts,
CreateNewAlerts,
- CustomDomainSettings,
DashboardPage,
DashboardWidget,
- EditAlertChannelsAlerts,
EditRulesPage,
ErrorDetails,
Home,
InfrastructureMonitoring,
- IngestionSettings,
InstalledIntegrations,
LicensePage,
ListAllALertsPage,
@@ -31,12 +27,10 @@ import {
LogsIndexToFields,
LogsSaveViews,
MetricsExplorer,
- MySettings,
NewDashboardPage,
OldLogsExplorer,
Onboarding,
OnboardingV2,
- OrganizationSettings,
OrgOnboarding,
PasswordReset,
PipelinePage,
@@ -45,7 +39,6 @@ import {
ServicesTablePage,
ServiceTopLevelOperationsPage,
SettingsPage,
- ShortcutsPage,
SignupPage,
SomethingWentWrong,
StatusPage,
@@ -150,7 +143,7 @@ const routes: AppRoutes[] = [
},
{
path: ROUTES.SETTINGS,
- exact: true,
+ exact: false,
component: SettingsPage,
isPrivate: true,
key: 'SETTINGS',
@@ -260,13 +253,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'CHANNELS_NEW',
},
- {
- path: ROUTES.CHANNELS_EDIT,
- exact: true,
- component: EditAlertChannelsAlerts,
- isPrivate: true,
- key: 'CHANNELS_EDIT',
- },
{
path: ROUTES.ALL_CHANNELS,
exact: true,
@@ -295,41 +281,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'VERSION',
},
- {
- path: ROUTES.ORG_SETTINGS,
- exact: true,
- component: OrganizationSettings,
- isPrivate: true,
- key: 'ORG_SETTINGS',
- },
- {
- path: ROUTES.INGESTION_SETTINGS,
- exact: true,
- component: IngestionSettings,
- isPrivate: true,
- key: 'INGESTION_SETTINGS',
- },
- {
- path: ROUTES.API_KEYS,
- exact: true,
- component: APIKeys,
- isPrivate: true,
- key: 'API_KEYS',
- },
- {
- path: ROUTES.MY_SETTINGS,
- exact: true,
- component: MySettings,
- isPrivate: true,
- key: 'MY_SETTINGS',
- },
- {
- path: ROUTES.CUSTOM_DOMAIN_SETTINGS,
- exact: true,
- component: CustomDomainSettings,
- isPrivate: true,
- key: 'CUSTOM_DOMAIN_SETTINGS',
- },
{
path: ROUTES.LOGS,
exact: true,
@@ -393,13 +344,6 @@ const routes: AppRoutes[] = [
key: 'SOMETHING_WENT_WRONG',
isPrivate: false,
},
- {
- path: ROUTES.BILLING,
- exact: true,
- component: BillingPage,
- key: 'BILLING',
- isPrivate: true,
- },
{
path: ROUTES.WORKSPACE_LOCKED,
exact: true,
@@ -421,13 +365,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'WORKSPACE_ACCESS_RESTRICTED',
},
- {
- path: ROUTES.SHORTCUTS,
- exact: true,
- component: ShortcutsPage,
- isPrivate: true,
- key: 'SHORTCUTS',
- },
{
path: ROUTES.INTEGRATIONS,
exact: true,
@@ -498,6 +435,28 @@ const routes: AppRoutes[] = [
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
+
+ {
+ path: ROUTES.METER,
+ exact: true,
+ component: MeterExplorer,
+ key: 'METER',
+ isPrivate: true,
+ },
+ {
+ path: ROUTES.METER_EXPLORER,
+ exact: true,
+ component: MeterExplorer,
+ key: 'METER_EXPLORER',
+ isPrivate: true,
+ },
+ {
+ path: ROUTES.METER_EXPLORER_VIEWS,
+ exact: true,
+ component: MeterExplorer,
+ key: 'METER_EXPLORER_VIEWS',
+ isPrivate: true,
+ },
{
path: ROUTES.API_MONITORING,
exact: true,
diff --git a/frontend/src/api/APIKeys/createAPIKey.ts b/frontend/src/api/APIKeys/createAPIKey.ts
deleted file mode 100644
index 2b219a01662..00000000000
--- a/frontend/src/api/APIKeys/createAPIKey.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { APIKeyProps, CreateAPIKeyProps } from 'types/api/pat/types';
-
-const createAPIKey = async (
- props: CreateAPIKeyProps,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.post('/pats', {
- ...props,
- });
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default createAPIKey;
diff --git a/frontend/src/api/APIKeys/deleteAPIKey.ts b/frontend/src/api/APIKeys/deleteAPIKey.ts
deleted file mode 100644
index 03b8d595dad..00000000000
--- a/frontend/src/api/APIKeys/deleteAPIKey.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { AllAPIKeyProps } from 'types/api/pat/types';
-
-const deleteAPIKey = async (
- id: string,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.delete(`/pats/${id}`);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default deleteAPIKey;
diff --git a/frontend/src/api/APIKeys/getAPIKey.ts b/frontend/src/api/APIKeys/getAPIKey.ts
deleted file mode 100644
index c0410d873f0..00000000000
--- a/frontend/src/api/APIKeys/getAPIKey.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/alerts/get';
-
-const get = async (
- props: Props,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.get(`/pats/${props.id}`);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default get;
diff --git a/frontend/src/api/APIKeys/getAllAPIKeys.ts b/frontend/src/api/APIKeys/getAllAPIKeys.ts
deleted file mode 100644
index 488d9dc5cfe..00000000000
--- a/frontend/src/api/APIKeys/getAllAPIKeys.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import axios from 'api';
-import { AxiosResponse } from 'axios';
-import { AllAPIKeyProps } from 'types/api/pat/types';
-
-export const getAllAPIKeys = (): Promise> =>
- axios.get(`/pats`);
diff --git a/frontend/src/api/APIKeys/updateAPIKey.ts b/frontend/src/api/APIKeys/updateAPIKey.ts
deleted file mode 100644
index 38d20227a3b..00000000000
--- a/frontend/src/api/APIKeys/updateAPIKey.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, UpdateAPIKeyProps } from 'types/api/pat/types';
-
-const updateAPIKey = async (
- props: UpdateAPIKeyProps,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.put(`/pats/${props.id}`, {
- ...props.data,
- });
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default updateAPIKey;
diff --git a/frontend/src/api/SAML/deleteDomain.ts b/frontend/src/api/SAML/deleteDomain.ts
deleted file mode 100644
index 50c2b51a807..00000000000
--- a/frontend/src/api/SAML/deleteDomain.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/SAML/deleteDomain';
-
-const deleteDomain = async (
- props: Props,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.delete(`/domains/${props.id}`);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default deleteDomain;
diff --git a/frontend/src/api/SAML/listAllDomain.ts b/frontend/src/api/SAML/listAllDomain.ts
deleted file mode 100644
index 41620f7d3e5..00000000000
--- a/frontend/src/api/SAML/listAllDomain.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/SAML/listDomain';
-
-const listAllDomain = async (
- props: Props,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.get(`/orgs/${props.orgId}/domains`);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default listAllDomain;
diff --git a/frontend/src/api/SAML/postDomain.ts b/frontend/src/api/SAML/postDomain.ts
deleted file mode 100644
index 34a8ecd1f79..00000000000
--- a/frontend/src/api/SAML/postDomain.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/SAML/postDomain';
-
-const postDomain = async (
- props: Props,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.post(`/domains`, props);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default postDomain;
diff --git a/frontend/src/api/SAML/updateDomain.ts b/frontend/src/api/SAML/updateDomain.ts
deleted file mode 100644
index 0c4cce83af0..00000000000
--- a/frontend/src/api/SAML/updateDomain.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/SAML/updateDomain';
-
-const updateDomain = async (
- props: Props,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.put(`/domains/${props.id}`, props);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default updateDomain;
diff --git a/frontend/src/api/alerts/createAlertRule.ts b/frontend/src/api/alerts/createAlertRule.ts
new file mode 100644
index 00000000000..f993a244cfe
--- /dev/null
+++ b/frontend/src/api/alerts/createAlertRule.ts
@@ -0,0 +1,28 @@
+import axios from 'api';
+import { ErrorResponse, SuccessResponse } from 'types/api';
+import {
+ AlertRuleV2,
+ PostableAlertRuleV2,
+} from 'types/api/alerts/alertTypesV2';
+
+export interface CreateAlertRuleResponse {
+ data: AlertRuleV2;
+ status: string;
+}
+
+const createAlertRule = async (
+ props: PostableAlertRuleV2,
+): Promise | ErrorResponse> => {
+ const response = await axios.post(`/rules`, {
+ ...props,
+ });
+
+ return {
+ statusCode: 200,
+ error: null,
+ message: response.data.status,
+ payload: response.data.data,
+ };
+};
+
+export default createAlertRule;
diff --git a/frontend/src/api/alerts/testAlertRule.ts b/frontend/src/api/alerts/testAlertRule.ts
new file mode 100644
index 00000000000..6b2502f325c
--- /dev/null
+++ b/frontend/src/api/alerts/testAlertRule.ts
@@ -0,0 +1,28 @@
+import axios from 'api';
+import { ErrorResponse, SuccessResponse } from 'types/api';
+import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
+
+export interface TestAlertRuleResponse {
+ data: {
+ alertCount: number;
+ message: string;
+ };
+ status: string;
+}
+
+const testAlertRule = async (
+ props: PostableAlertRuleV2,
+): Promise | ErrorResponse> => {
+ const response = await axios.post(`/testRule`, {
+ ...props,
+ });
+
+ return {
+ statusCode: 200,
+ error: null,
+ message: response.data.status,
+ payload: response.data.data,
+ };
+};
+
+export default testAlertRule;
diff --git a/frontend/src/api/alerts/updateAlertRule.ts b/frontend/src/api/alerts/updateAlertRule.ts
new file mode 100644
index 00000000000..6553d685338
--- /dev/null
+++ b/frontend/src/api/alerts/updateAlertRule.ts
@@ -0,0 +1,26 @@
+import axios from 'api';
+import { ErrorResponse, SuccessResponse } from 'types/api';
+import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
+
+export interface UpdateAlertRuleResponse {
+ data: string;
+ status: string;
+}
+
+const updateAlertRule = async (
+ id: string,
+ postableAlertRule: PostableAlertRuleV2,
+): Promise | ErrorResponse> => {
+ const response = await axios.put(`/rules/${id}`, {
+ ...postableAlertRule,
+ });
+
+ return {
+ statusCode: 200,
+ error: null,
+ message: response.data.status,
+ payload: response.data.data,
+ };
+};
+
+export default updateAlertRule;
diff --git a/frontend/src/api/apiV1.ts b/frontend/src/api/apiV1.ts
index abd7d701a4c..f7f15f0cce7 100644
--- a/frontend/src/api/apiV1.ts
+++ b/frontend/src/api/apiV1.ts
@@ -3,6 +3,7 @@ const apiV1 = '/api/v1/';
export const apiV2 = '/api/v2/';
export const apiV3 = '/api/v3/';
export const apiV4 = '/api/v4/';
+export const apiV5 = '/api/v5/';
export const gatewayApiV1 = '/api/gateway/v1/';
export const gatewayApiV2 = '/api/gateway/v2/';
export const apiAlertManager = '/api/alertmanager/';
diff --git a/frontend/src/api/billing/checkout.ts b/frontend/src/api/billing/checkout.ts
deleted file mode 100644
index f8eaf397486..00000000000
--- a/frontend/src/api/billing/checkout.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import {
- CheckoutRequestPayloadProps,
- CheckoutSuccessPayloadProps,
-} from 'types/api/billing/checkout';
-
-const updateCreditCardApi = async (
- props: CheckoutRequestPayloadProps,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.post('/checkout', {
- url: props.url,
- });
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default updateCreditCardApi;
diff --git a/frontend/src/api/billing/manage.ts b/frontend/src/api/billing/manage.ts
deleted file mode 100644
index 1ea8fa762d3..00000000000
--- a/frontend/src/api/billing/manage.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import {
- CheckoutRequestPayloadProps,
- CheckoutSuccessPayloadProps,
-} from 'types/api/billing/checkout';
-
-const manageCreditCardApi = async (
- props: CheckoutRequestPayloadProps,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.post('/portal', {
- url: props.url,
- });
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default manageCreditCardApi;
diff --git a/frontend/src/api/changelog/getChangelogByVersion.ts b/frontend/src/api/changelog/getChangelogByVersion.ts
new file mode 100644
index 00000000000..b9b3f1f9c93
--- /dev/null
+++ b/frontend/src/api/changelog/getChangelogByVersion.ts
@@ -0,0 +1,47 @@
+import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
+import axios, { AxiosError } from 'axios';
+import { ErrorResponse, SuccessResponse } from 'types/api';
+import {
+ ChangelogSchema,
+ DeploymentType,
+} from 'types/api/changelog/getChangelogByVersion';
+
+const getChangelogByVersion = async (
+ versionId: string,
+ deployment_type?: DeploymentType,
+): Promise | ErrorResponse> => {
+ try {
+ let queryParams = `filters[version][$eq]=${versionId}&populate[features][sort]=sort_order:asc&populate[features][populate][media][fields]=id,ext,url,mime,alternativeText`;
+
+ if (
+ deployment_type &&
+ Object.values(DeploymentType).includes(deployment_type)
+ ) {
+ const excludedDeploymentType =
+ deployment_type === DeploymentType.CLOUD_ONLY
+ ? DeploymentType.OSS_ONLY
+ : DeploymentType.CLOUD_ONLY;
+
+ queryParams = `${queryParams}&populate[features][filters][deployment_type][$notIn]=${excludedDeploymentType}`;
+ }
+
+ const response = await axios.get(`
+ https://cms.signoz.cloud/api/release-changelogs?${queryParams}
+ `);
+
+ if (!Array.isArray(response.data.data) || response.data.data.length === 0) {
+ throw new Error('No changelog found!');
+ }
+
+ return {
+ statusCode: 200,
+ error: null,
+ message: response.statusText,
+ payload: response.data.data[0],
+ };
+ } catch (error) {
+ return ErrorResponseHandler(error as AxiosError);
+ }
+};
+
+export default getChangelogByVersion;
diff --git a/frontend/src/api/dashboard/create.ts b/frontend/src/api/dashboard/create.ts
deleted file mode 100644
index bf5458ac40a..00000000000
--- a/frontend/src/api/dashboard/create.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/dashboard/create';
-
-const createDashboard = async (
- props: Props,
-): Promise | ErrorResponse> => {
- const url = props.uploadedGrafana ? '/dashboards/grafana' : '/dashboards';
- try {
- const response = await axios.post(url, {
- ...props,
- });
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default createDashboard;
diff --git a/frontend/src/api/dashboard/delete.ts b/frontend/src/api/dashboard/delete.ts
deleted file mode 100644
index 8faf7113830..00000000000
--- a/frontend/src/api/dashboard/delete.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import axios from 'api';
-import { PayloadProps, Props } from 'types/api/dashboard/delete';
-
-const deleteDashboard = (props: Props): Promise =>
- axios
- .delete(`/dashboards/${props.uuid}`)
- .then((response) => response.data);
-
-export default deleteDashboard;
diff --git a/frontend/src/api/dashboard/get.ts b/frontend/src/api/dashboard/get.ts
deleted file mode 100644
index 01e04c6c0f4..00000000000
--- a/frontend/src/api/dashboard/get.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import axios from 'api';
-import { ApiResponse } from 'types/api';
-import { Props } from 'types/api/dashboard/get';
-import { Dashboard } from 'types/api/dashboard/getAll';
-
-const getDashboard = (props: Props): Promise =>
- axios
- .get>(`/dashboards/${props.uuid}`)
- .then((res) => res.data.data);
-
-export default getDashboard;
diff --git a/frontend/src/api/dashboard/getAll.ts b/frontend/src/api/dashboard/getAll.ts
deleted file mode 100644
index aafe44b3ed1..00000000000
--- a/frontend/src/api/dashboard/getAll.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import axios from 'api';
-import { ApiResponse } from 'types/api';
-import { Dashboard } from 'types/api/dashboard/getAll';
-
-export const getAllDashboardList = (): Promise =>
- axios
- .get>('/dashboards')
- .then((res) => res.data.data);
diff --git a/frontend/src/api/dashboard/lockDashboard.ts b/frontend/src/api/dashboard/lockDashboard.ts
deleted file mode 100644
index 3393de8fa3a..00000000000
--- a/frontend/src/api/dashboard/lockDashboard.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import axios from 'api';
-import { AxiosResponse } from 'axios';
-
-interface LockDashboardProps {
- uuid: string;
-}
-
-const lockDashboard = (props: LockDashboardProps): Promise =>
- axios.put(`/dashboards/${props.uuid}/lock`);
-
-export default lockDashboard;
diff --git a/frontend/src/api/dashboard/substitute_vars.ts b/frontend/src/api/dashboard/substitute_vars.ts
new file mode 100644
index 00000000000..2ab57b2e42c
--- /dev/null
+++ b/frontend/src/api/dashboard/substitute_vars.ts
@@ -0,0 +1,34 @@
+import { ApiV5Instance } from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { QueryRangePayloadV5 } from 'api/v5/v5';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
+
+interface ISubstituteVars {
+ compositeQuery: ICompositeMetricQuery;
+}
+
+export const getSubstituteVars = async (
+ props?: Partial,
+ signal?: AbortSignal,
+ headers?: Record,
+): Promise> => {
+ try {
+ const response = await ApiV5Instance.post<{ data: ISubstituteVars }>(
+ '/substitute_vars',
+ props,
+ {
+ signal,
+ headers,
+ },
+ );
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
diff --git a/frontend/src/api/dashboard/unlockDashboard.ts b/frontend/src/api/dashboard/unlockDashboard.ts
deleted file mode 100644
index fd4ffbe41a0..00000000000
--- a/frontend/src/api/dashboard/unlockDashboard.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import axios from 'api';
-import { AxiosResponse } from 'axios';
-
-interface UnlockDashboardProps {
- uuid: string;
-}
-
-const unlockDashboard = (props: UnlockDashboardProps): Promise =>
- axios.put(`/dashboards/${props.uuid}/unlock`);
-
-export default unlockDashboard;
diff --git a/frontend/src/api/dashboard/update.ts b/frontend/src/api/dashboard/update.ts
deleted file mode 100644
index 21216e051f3..00000000000
--- a/frontend/src/api/dashboard/update.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import axios from 'api';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/dashboard/update';
-
-const updateDashboard = async (
- props: Props,
-): Promise | ErrorResponse> => {
- const response = await axios.put(`/dashboards/${props.uuid}`, {
- ...props.data,
- });
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
-};
-
-export default updateDashboard;
diff --git a/frontend/src/api/dynamicVariables/__tests__/getFieldKeys.test.ts b/frontend/src/api/dynamicVariables/__tests__/getFieldKeys.test.ts
new file mode 100644
index 00000000000..c94c915b622
--- /dev/null
+++ b/frontend/src/api/dynamicVariables/__tests__/getFieldKeys.test.ts
@@ -0,0 +1,115 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+import { ApiBaseInstance } from 'api';
+
+import { getFieldKeys } from '../getFieldKeys';
+
+// Mock the API instance
+jest.mock('api', () => ({
+ ApiBaseInstance: {
+ get: jest.fn(),
+ },
+}));
+
+describe('getFieldKeys API', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockSuccessResponse = {
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ keys: {
+ 'service.name': [],
+ 'http.status_code': [],
+ },
+ complete: true,
+ },
+ },
+ };
+
+ it('should call API with correct parameters when no args provided', async () => {
+ // Mock successful API response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
+
+ // Call function with no parameters
+ await getFieldKeys();
+
+ // Verify API was called correctly with empty params object
+ expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
+ params: {},
+ });
+ });
+
+ it('should call API with signal parameter when provided', async () => {
+ // Mock successful API response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
+
+ // Call function with signal parameter
+ await getFieldKeys('traces');
+
+ // Verify API was called with signal parameter
+ expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
+ params: { signal: 'traces' },
+ });
+ });
+
+ it('should call API with name parameter when provided', async () => {
+ // Mock successful API response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ keys: { service: [] },
+ complete: false,
+ },
+ },
+ });
+
+ // Call function with name parameter
+ await getFieldKeys(undefined, 'service');
+
+ // Verify API was called with name parameter
+ expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
+ params: { name: 'service' },
+ });
+ });
+
+ it('should call API with both signal and name when provided', async () => {
+ // Mock successful API response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ keys: { service: [] },
+ complete: false,
+ },
+ },
+ });
+
+ // Call function with both parameters
+ await getFieldKeys('logs', 'service');
+
+ // Verify API was called with both parameters
+ expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
+ params: { signal: 'logs', name: 'service' },
+ });
+ });
+
+ it('should return properly formatted response', async () => {
+ // Mock API to return our response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
+
+ // Call the function
+ const result = await getFieldKeys('traces');
+
+ // Verify the returned structure matches SuccessResponseV2 format
+ expect(result).toEqual({
+ httpStatusCode: 200,
+ data: mockSuccessResponse.data.data,
+ });
+ });
+});
diff --git a/frontend/src/api/dynamicVariables/__tests__/getFieldValues.test.ts b/frontend/src/api/dynamicVariables/__tests__/getFieldValues.test.ts
new file mode 100644
index 00000000000..896e939e9a4
--- /dev/null
+++ b/frontend/src/api/dynamicVariables/__tests__/getFieldValues.test.ts
@@ -0,0 +1,214 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+import { ApiBaseInstance } from 'api';
+
+import { getFieldValues } from '../getFieldValues';
+
+// Mock the API instance
+jest.mock('api', () => ({
+ ApiBaseInstance: {
+ get: jest.fn(),
+ },
+}));
+
+describe('getFieldValues API', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should call the API with correct parameters (no options)', async () => {
+ // Mock API response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ values: {
+ stringValues: ['frontend', 'backend'],
+ },
+ complete: true,
+ },
+ },
+ });
+
+ // Call function without parameters
+ await getFieldValues();
+
+ // Verify API was called correctly with empty params
+ expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
+ params: {},
+ });
+ });
+
+ it('should call the API with signal parameter', async () => {
+ // Mock API response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ values: {
+ stringValues: ['frontend', 'backend'],
+ },
+ complete: true,
+ },
+ },
+ });
+
+ // Call function with signal parameter
+ await getFieldValues('traces');
+
+ // Verify API was called with signal parameter
+ expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
+ params: { signal: 'traces' },
+ });
+ });
+
+ it('should call the API with name parameter', async () => {
+ // Mock API response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ values: {
+ stringValues: ['frontend', 'backend'],
+ },
+ complete: true,
+ },
+ },
+ });
+
+ // Call function with name parameter
+ await getFieldValues(undefined, 'service.name');
+
+ // Verify API was called with name parameter
+ expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
+ params: { name: 'service.name' },
+ });
+ });
+
+ it('should call the API with value parameter', async () => {
+ // Mock API response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ values: {
+ stringValues: ['frontend'],
+ },
+ complete: false,
+ },
+ },
+ });
+
+ // Call function with value parameter
+ await getFieldValues(undefined, 'service.name', 'front');
+
+ // Verify API was called with value parameter
+ expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
+ params: { name: 'service.name', searchText: 'front' },
+ });
+ });
+
+ it('should call the API with time range parameters', async () => {
+ // Mock API response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ values: {
+ stringValues: ['frontend', 'backend'],
+ },
+ complete: true,
+ },
+ },
+ });
+
+ // Call function with time range parameters
+ const startUnixMilli = 1625097600000000; // Note: nanoseconds
+ const endUnixMilli = 1625184000000000;
+ await getFieldValues(
+ 'logs',
+ 'service.name',
+ undefined,
+ startUnixMilli,
+ endUnixMilli,
+ );
+
+ // Verify API was called with time range parameters (converted to milliseconds)
+ expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
+ params: {
+ signal: 'logs',
+ name: 'service.name',
+ startUnixMilli: '1625097600', // Should be converted to seconds (divided by 1000000)
+ endUnixMilli: '1625184000', // Should be converted to seconds (divided by 1000000)
+ },
+ });
+ });
+
+ it('should normalize the response values', async () => {
+ // Mock API response with multiple value types
+ const mockResponse = {
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ values: {
+ stringValues: ['frontend', 'backend'],
+ numberValues: [200, 404],
+ boolValues: [true, false],
+ },
+ complete: true,
+ },
+ },
+ };
+
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockResponse);
+
+ // Call the function
+ const result = await getFieldValues('traces', 'mixed.values');
+
+ // Verify the response has normalized values array
+ expect(result.data?.normalizedValues).toContain('frontend');
+ expect(result.data?.normalizedValues).toContain('backend');
+ expect(result.data?.normalizedValues).toContain('200');
+ expect(result.data?.normalizedValues).toContain('404');
+ expect(result.data?.normalizedValues).toContain('true');
+ expect(result.data?.normalizedValues).toContain('false');
+ expect(result.data?.normalizedValues?.length).toBe(6);
+ });
+
+ it('should return a properly formatted success response', async () => {
+ // Create mock response
+ const mockApiResponse = {
+ status: 200,
+ data: {
+ status: 'success',
+ data: {
+ values: {
+ stringValues: ['frontend', 'backend'],
+ },
+ complete: true,
+ },
+ },
+ };
+
+ // Mock API to return our response
+ (ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockApiResponse);
+
+ // Call the function
+ const result = await getFieldValues('traces', 'service.name');
+
+ // Verify the returned structure matches SuccessResponseV2 format
+ expect(result).toEqual({
+ httpStatusCode: 200,
+ data: expect.objectContaining({
+ values: expect.any(Object),
+ normalizedValues: expect.any(Array),
+ complete: true,
+ }),
+ });
+ });
+});
diff --git a/frontend/src/api/dynamicVariables/getFieldKeys.ts b/frontend/src/api/dynamicVariables/getFieldKeys.ts
new file mode 100644
index 00000000000..12bfdf1d1e6
--- /dev/null
+++ b/frontend/src/api/dynamicVariables/getFieldKeys.ts
@@ -0,0 +1,38 @@
+import { ApiBaseInstance } from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { FieldKeyResponse } from 'types/api/dynamicVariables/getFieldKeys';
+
+/**
+ * Get field keys for a given signal type
+ * @param signal Type of signal (traces, logs, metrics)
+ * @param name Optional search text
+ */
+export const getFieldKeys = async (
+ signal?: 'traces' | 'logs' | 'metrics',
+ name?: string,
+): Promise> => {
+ const params: Record = {};
+
+ if (signal) {
+ params.signal = encodeURIComponent(signal);
+ }
+
+ if (name) {
+ params.name = encodeURIComponent(name);
+ }
+
+ try {
+ const response = await ApiBaseInstance.get('/fields/keys', { params });
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default getFieldKeys;
diff --git a/frontend/src/api/dynamicVariables/getFieldValues.ts b/frontend/src/api/dynamicVariables/getFieldValues.ts
new file mode 100644
index 00000000000..6e0c60ec2b2
--- /dev/null
+++ b/frontend/src/api/dynamicVariables/getFieldValues.ts
@@ -0,0 +1,87 @@
+/* eslint-disable sonarjs/cognitive-complexity */
+import { ApiBaseInstance } from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues';
+
+/**
+ * Get field values for a given signal type and field name
+ * @param signal Type of signal (traces, logs, metrics)
+ * @param name Name of the attribute for which values are being fetched
+ * @param value Optional search text
+ * @param existingQuery Optional existing query - across all present dynamic variables
+ */
+export const getFieldValues = async (
+ signal?: 'traces' | 'logs' | 'metrics',
+ name?: string,
+ searchText?: string,
+ startUnixMilli?: number,
+ endUnixMilli?: number,
+ existingQuery?: string,
+): Promise> => {
+ const params: Record = {};
+
+ if (signal) {
+ params.signal = encodeURIComponent(signal);
+ }
+
+ if (name) {
+ params.name = encodeURIComponent(name);
+ }
+
+ if (searchText) {
+ params.searchText = encodeURIComponent(searchText);
+ }
+
+ if (startUnixMilli) {
+ params.startUnixMilli = Math.floor(startUnixMilli / 1000000).toString();
+ }
+
+ if (endUnixMilli) {
+ params.endUnixMilli = Math.floor(endUnixMilli / 1000000).toString();
+ }
+
+ if (existingQuery) {
+ params.existingQuery = existingQuery;
+ }
+
+ try {
+ const response = await ApiBaseInstance.get('/fields/values', { params });
+
+ // Normalize values from different types (stringValues, boolValues, etc.)
+ if (response.data?.data?.values) {
+ const allValues: string[] = [];
+ Object.entries(response.data?.data?.values).forEach(
+ ([key, valueArray]: [string, any]) => {
+ // Skip RelatedValues as they should be kept separate
+ if (key === 'relatedValues') {
+ return;
+ }
+
+ if (Array.isArray(valueArray)) {
+ allValues.push(...valueArray.map(String));
+ }
+ },
+ );
+
+ // Add a normalized values array to the response
+ response.data.data.normalizedValues = allValues;
+
+ // Add relatedValues to the response as per FieldValueResponse
+ if (response.data?.data?.values?.relatedValues) {
+ response.data.data.relatedValues =
+ response.data?.data?.values?.relatedValues;
+ }
+ }
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default getFieldValues;
diff --git a/frontend/src/api/features/getFeatureFlags.ts b/frontend/src/api/features/getFeatureFlags.ts
deleted file mode 100644
index 2ce37b99e15..00000000000
--- a/frontend/src/api/features/getFeatureFlags.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import axios from 'api';
-import { ApiResponse } from 'types/api';
-import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
-
-const getFeaturesFlags = (): Promise =>
- axios
- .get>(`/featureFlags`)
- .then((response) => response.data.data);
-
-export default getFeaturesFlags;
diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts
index f58e2405163..9e78b902212 100644
--- a/frontend/src/api/index.ts
+++ b/frontend/src/api/index.ts
@@ -4,7 +4,11 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import loginApi from 'api/v1/login/login';
import afterLogin from 'AppRoutes/utils';
-import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
+import axios, {
+ AxiosError,
+ AxiosResponse,
+ InternalAxiosRequestConfig,
+} from 'axios';
import { ENVIRONMENT } from 'constants/env';
import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -15,6 +19,7 @@ import apiV1, {
apiV2,
apiV3,
apiV4,
+ apiV5,
gatewayApiV1,
gatewayApiV2,
} from './apiV1';
@@ -83,24 +88,27 @@ const interceptorRejected = async (
true,
);
- const reResponse = await axios(
- `${value.config.baseURL}${value.config.url?.substring(1)}`,
- {
- method: value.config.method,
- headers: {
- ...value.config.headers,
- Authorization: `Bearer ${response.data.accessJwt}`,
+ try {
+ const reResponse = await axios(
+ `${value.config.baseURL}${value.config.url?.substring(1)}`,
+ {
+ method: value.config.method,
+ headers: {
+ ...value.config.headers,
+ Authorization: `Bearer ${response.data.accessJwt}`,
+ },
+ data: {
+ ...JSON.parse(value.config.data || '{}'),
+ },
},
- data: {
- ...JSON.parse(value.config.data || '{}'),
- },
- },
- );
- if (reResponse.status === 200) {
+ );
+
return await Promise.resolve(reResponse);
+ } catch (error) {
+ if ((error as AxiosError)?.response?.status === 401) {
+ Logout();
+ }
}
- Logout();
- return await Promise.reject(reResponse);
} catch (error) {
Logout();
}
@@ -164,6 +172,18 @@ ApiV4Instance.interceptors.response.use(
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
//
+// axios V5
+export const ApiV5Instance = axios.create({
+ baseURL: `${ENVIRONMENT.baseURL}${apiV5}`,
+});
+
+ApiV5Instance.interceptors.response.use(
+ interceptorsResponse,
+ interceptorRejected,
+);
+ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
+//
+
// axios Base
export const ApiBaseInstance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
diff --git a/frontend/src/api/infraMonitoring/getK8sClustersList.ts b/frontend/src/api/infraMonitoring/getK8sClustersList.ts
index 2da1b214b31..b08090ed2a2 100644
--- a/frontend/src/api/infraMonitoring/getK8sClustersList.ts
+++ b/frontend/src/api/infraMonitoring/getK8sClustersList.ts
@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { UnderscoreToDotMap } from '../utils';
+
export interface K8sClustersListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -40,23 +42,80 @@ export interface K8sClustersListResponse {
};
}
+export const clustersMetaMap = [
+ { dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
+ { dot: 'k8s.cluster.uid', under: 'k8s_cluster_uid' },
+] as const;
+
+export function mapClustersMeta(
+ raw: Record,
+): K8sClustersData['meta'] {
+ const out: Record = { ...raw };
+ clustersMetaMap.forEach(({ dot, under }) => {
+ if (dot in raw) {
+ const v = raw[dot];
+ out[under] = typeof v === 'string' ? v : raw[under];
+ }
+ });
+ return out as K8sClustersData['meta'];
+}
+
export const getK8sClustersList = async (
props: K8sClustersListPayload,
signal?: AbortSignal,
headers?: Record,
+ dotMetricsEnabled = false,
): Promise | ErrorResponse> => {
try {
- const response = await axios.post('/clusters/list', props, {
+ const requestProps =
+ dotMetricsEnabled && Array.isArray(props.filters?.items)
+ ? {
+ ...props,
+ filters: {
+ ...props.filters,
+ items: props.filters.items.reduce(
+ (acc, item) => {
+ if (item.value === undefined) return acc;
+ if (
+ item.key &&
+ typeof item.key === 'object' &&
+ 'key' in item.key &&
+ typeof item.key.key === 'string'
+ ) {
+ const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
+ acc.push({
+ ...item,
+ key: { ...item.key, key: mappedKey },
+ });
+ } else {
+ acc.push(item);
+ }
+ return acc;
+ },
+ [] as typeof props.filters.items,
+ ),
+ },
+ }
+ : props;
+
+ const response = await axios.post('/clusters/list', requestProps, {
signal,
headers,
});
+ const payload: K8sClustersListResponse = response.data;
+
+ // one-liner meta mapping
+ payload.data.records = payload.data.records.map((record) => ({
+ ...record,
+ meta: mapClustersMeta(record.meta as Record),
+ }));
return {
statusCode: 200,
error: null,
message: 'Success',
- payload: response.data,
- params: props,
+ payload,
+ params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
diff --git a/frontend/src/api/infraMonitoring/getK8sDaemonSetsList.ts b/frontend/src/api/infraMonitoring/getK8sDaemonSetsList.ts
index c09de10580e..a35f8871eca 100644
--- a/frontend/src/api/infraMonitoring/getK8sDaemonSetsList.ts
+++ b/frontend/src/api/infraMonitoring/getK8sDaemonSetsList.ts
@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { UnderscoreToDotMap } from '../utils';
+
export interface K8sDaemonSetsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -46,23 +48,82 @@ export interface K8sDaemonSetsListResponse {
};
}
+export const daemonSetsMetaMap = [
+ { dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
+ { dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
+ { dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
+] as const;
+
+export function mapDaemonSetsMeta(
+ raw: Record,
+): K8sDaemonSetsData['meta'] {
+ const out: Record = { ...raw };
+ daemonSetsMetaMap.forEach(({ dot, under }) => {
+ if (dot in raw) {
+ const v = raw[dot];
+ out[under] = typeof v === 'string' ? v : raw[under];
+ }
+ });
+ return out as K8sDaemonSetsData['meta'];
+}
+
export const getK8sDaemonSetsList = async (
props: K8sDaemonSetsListPayload,
signal?: AbortSignal,
headers?: Record,
+ dotMetricsEnabled = false,
): Promise | ErrorResponse> => {
try {
- const response = await axios.post('/daemonsets/list', props, {
+ // filter prep (unchanged)…
+ const requestProps =
+ dotMetricsEnabled && Array.isArray(props.filters?.items)
+ ? {
+ ...props,
+ filters: {
+ ...props.filters,
+ items: props.filters.items.reduce(
+ (acc, item) => {
+ if (item.value === undefined) return acc;
+ if (
+ item.key &&
+ typeof item.key === 'object' &&
+ 'key' in item.key &&
+ typeof item.key.key === 'string'
+ ) {
+ const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
+ acc.push({
+ ...item,
+ key: { ...item.key, key: mappedKey },
+ });
+ } else {
+ acc.push(item);
+ }
+ return acc;
+ },
+ [] as typeof props.filters.items,
+ ),
+ },
+ }
+ : props;
+
+ const response = await axios.post('/daemonsets/list', requestProps, {
signal,
headers,
});
+ const payload: K8sDaemonSetsListResponse = response.data;
+
+ // single-line meta mapping
+ payload.data.records = payload.data.records.map((record) => ({
+ ...record,
+ meta: mapDaemonSetsMeta(record.meta as Record),
+ }));
return {
statusCode: 200,
error: null,
message: 'Success',
- payload: response.data,
- params: props,
+ payload,
+ params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
diff --git a/frontend/src/api/infraMonitoring/getK8sDeploymentsList.ts b/frontend/src/api/infraMonitoring/getK8sDeploymentsList.ts
index b991ce22e7c..f39c62c88bc 100644
--- a/frontend/src/api/infraMonitoring/getK8sDeploymentsList.ts
+++ b/frontend/src/api/infraMonitoring/getK8sDeploymentsList.ts
@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { UnderscoreToDotMap } from '../utils';
+
export interface K8sDeploymentsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -46,23 +48,81 @@ export interface K8sDeploymentsListResponse {
};
}
+export const deploymentsMetaMap = [
+ { dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
+ { dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
+ { dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
+] as const;
+
+export function mapDeploymentsMeta(
+ raw: Record,
+): K8sDeploymentsData['meta'] {
+ const out: Record = { ...raw };
+ deploymentsMetaMap.forEach(({ dot, under }) => {
+ if (dot in raw) {
+ const v = raw[dot];
+ out[under] = typeof v === 'string' ? v : raw[under];
+ }
+ });
+ return out as K8sDeploymentsData['meta'];
+}
+
export const getK8sDeploymentsList = async (
props: K8sDeploymentsListPayload,
signal?: AbortSignal,
headers?: Record,
+ dotMetricsEnabled = false,
): Promise | ErrorResponse> => {
try {
- const response = await axios.post('/deployments/list', props, {
+ const requestProps =
+ dotMetricsEnabled && Array.isArray(props.filters?.items)
+ ? {
+ ...props,
+ filters: {
+ ...props.filters,
+ items: props.filters.items.reduce(
+ (acc, item) => {
+ if (item.value === undefined) return acc;
+ if (
+ item.key &&
+ typeof item.key === 'object' &&
+ 'key' in item.key &&
+ typeof item.key.key === 'string'
+ ) {
+ const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
+ acc.push({
+ ...item,
+ key: { ...item.key, key: mappedKey },
+ });
+ } else {
+ acc.push(item);
+ }
+ return acc;
+ },
+ [] as typeof props.filters.items,
+ ),
+ },
+ }
+ : props;
+
+ const response = await axios.post('/deployments/list', requestProps, {
signal,
headers,
});
+ const payload: K8sDeploymentsListResponse = response.data;
+
+ // single-line mapping
+ payload.data.records = payload.data.records.map((record) => ({
+ ...record,
+ meta: mapDeploymentsMeta(record.meta as Record),
+ }));
return {
statusCode: 200,
error: null,
message: 'Success',
- payload: response.data,
- params: props,
+ payload,
+ params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
diff --git a/frontend/src/api/infraMonitoring/getK8sJobsList.ts b/frontend/src/api/infraMonitoring/getK8sJobsList.ts
index 36a6bb973dc..828e42a79de 100644
--- a/frontend/src/api/infraMonitoring/getK8sJobsList.ts
+++ b/frontend/src/api/infraMonitoring/getK8sJobsList.ts
@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { UnderscoreToDotMap } from '../utils';
+
export interface K8sJobsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -48,23 +50,79 @@ export interface K8sJobsListResponse {
};
}
+export const jobsMetaMap = [
+ { dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
+ { dot: 'k8s.job.name', under: 'k8s_job_name' },
+ { dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
+] as const;
+
+export function mapJobsMeta(raw: Record): K8sJobsData['meta'] {
+ const out: Record = { ...raw };
+ jobsMetaMap.forEach(({ dot, under }) => {
+ if (dot in raw) {
+ const v = raw[dot];
+ out[under] = typeof v === 'string' ? v : raw[under];
+ }
+ });
+ return out as K8sJobsData['meta'];
+}
+
export const getK8sJobsList = async (
props: K8sJobsListPayload,
signal?: AbortSignal,
headers?: Record,
+ dotMetricsEnabled = false,
): Promise | ErrorResponse> => {
try {
- const response = await axios.post('/jobs/list', props, {
+ const requestProps =
+ dotMetricsEnabled && Array.isArray(props.filters?.items)
+ ? {
+ ...props,
+ filters: {
+ ...props.filters,
+ items: props.filters.items.reduce(
+ (acc, item) => {
+ if (item.value === undefined) return acc;
+ if (
+ item.key &&
+ typeof item.key === 'object' &&
+ 'key' in item.key &&
+ typeof item.key.key === 'string'
+ ) {
+ const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
+ acc.push({
+ ...item,
+ key: { ...item.key, key: mappedKey },
+ });
+ } else {
+ acc.push(item);
+ }
+ return acc;
+ },
+ [] as typeof props.filters.items,
+ ),
+ },
+ }
+ : props;
+
+ const response = await axios.post('/jobs/list', requestProps, {
signal,
headers,
});
+ const payload: K8sJobsListResponse = response.data;
+
+ // one-liner meta mapping
+ payload.data.records = payload.data.records.map((record) => ({
+ ...record,
+ meta: mapJobsMeta(record.meta as Record),
+ }));
return {
statusCode: 200,
error: null,
message: 'Success',
- payload: response.data,
- params: props,
+ payload,
+ params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
diff --git a/frontend/src/api/infraMonitoring/getK8sNamespacesList.ts b/frontend/src/api/infraMonitoring/getK8sNamespacesList.ts
index bba2249f4e9..a492aac0453 100644
--- a/frontend/src/api/infraMonitoring/getK8sNamespacesList.ts
+++ b/frontend/src/api/infraMonitoring/getK8sNamespacesList.ts
@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { UnderscoreToDotMap } from '../utils';
+
export interface K8sNamespacesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -38,23 +40,79 @@ export interface K8sNamespacesListResponse {
};
}
+export const namespacesMetaMap = [
+ { dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
+ { dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
+] as const;
+
+export function mapNamespacesMeta(
+ raw: Record,
+): K8sNamespacesData['meta'] {
+ const out: Record = { ...raw };
+ namespacesMetaMap.forEach(({ dot, under }) => {
+ if (dot in raw) {
+ const v = raw[dot];
+ out[under] = typeof v === 'string' ? v : raw[under];
+ }
+ });
+ return out as K8sNamespacesData['meta'];
+}
+
export const getK8sNamespacesList = async (
props: K8sNamespacesListPayload,
signal?: AbortSignal,
headers?: Record,
+ dotMetricsEnabled = false,
): Promise | ErrorResponse> => {
try {
- const response = await axios.post('/namespaces/list', props, {
+ const requestProps =
+ dotMetricsEnabled && Array.isArray(props.filters?.items)
+ ? {
+ ...props,
+ filters: {
+ ...props.filters,
+ items: props.filters.items.reduce(
+ (acc, item) => {
+ if (item.value === undefined) return acc;
+ if (
+ item.key &&
+ typeof item.key === 'object' &&
+ 'key' in item.key &&
+ typeof item.key.key === 'string'
+ ) {
+ const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
+ acc.push({
+ ...item,
+ key: { ...item.key, key: mappedKey },
+ });
+ } else {
+ acc.push(item);
+ }
+ return acc;
+ },
+ [] as typeof props.filters.items,
+ ),
+ },
+ }
+ : props;
+
+ const response = await axios.post('/namespaces/list', requestProps, {
signal,
headers,
});
+ const payload: K8sNamespacesListResponse = response.data;
+
+ payload.data.records = payload.data.records.map((record) => ({
+ ...record,
+ meta: mapNamespacesMeta(record.meta as Record),
+ }));
return {
statusCode: 200,
error: null,
message: 'Success',
- payload: response.data,
- params: props,
+ payload,
+ params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
diff --git a/frontend/src/api/infraMonitoring/getK8sNodesList.ts b/frontend/src/api/infraMonitoring/getK8sNodesList.ts
index 71228b030b5..d0045f4820f 100644
--- a/frontend/src/api/infraMonitoring/getK8sNodesList.ts
+++ b/frontend/src/api/infraMonitoring/getK8sNodesList.ts
@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { UnderscoreToDotMap } from '../utils';
+
export interface K8sNodesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -41,23 +43,81 @@ export interface K8sNodesListResponse {
};
}
+export const nodesMetaMap = [
+ { dot: 'k8s.node.name', under: 'k8s_node_name' },
+ { dot: 'k8s.node.uid', under: 'k8s_node_uid' },
+ { dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
+] as const;
+
+export function mapNodesMeta(
+ raw: Record,
+): K8sNodesData['meta'] {
+ const out: Record = { ...raw };
+ nodesMetaMap.forEach(({ dot, under }) => {
+ if (dot in raw) {
+ const v = raw[dot];
+ out[under] = typeof v === 'string' ? v : raw[under];
+ }
+ });
+ return out as K8sNodesData['meta'];
+}
+
export const getK8sNodesList = async (
props: K8sNodesListPayload,
signal?: AbortSignal,
headers?: Record,
+ dotMetricsEnabled = false,
): Promise | ErrorResponse> => {
try {
- const response = await axios.post('/nodes/list', props, {
+ const requestProps =
+ dotMetricsEnabled && Array.isArray(props.filters?.items)
+ ? {
+ ...props,
+ filters: {
+ ...props.filters,
+ items: props.filters.items.reduce(
+ (acc, item) => {
+ if (item.value === undefined) return acc;
+ if (
+ item.key &&
+ typeof item.key === 'object' &&
+ 'key' in item.key &&
+ typeof item.key.key === 'string'
+ ) {
+ const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
+ acc.push({
+ ...item,
+ key: { ...item.key, key: mappedKey },
+ });
+ } else {
+ acc.push(item);
+ }
+ return acc;
+ },
+ [] as typeof props.filters.items,
+ ),
+ },
+ }
+ : props;
+
+ const response = await axios.post('/nodes/list', requestProps, {
signal,
headers,
});
+ const payload: K8sNodesListResponse = response.data;
+
+ // one-liner to map dot→underscore
+ payload.data.records = payload.data.records.map((record) => ({
+ ...record,
+ meta: mapNodesMeta(record.meta as Record),
+ }));
return {
statusCode: 200,
error: null,
message: 'Success',
- payload: response.data,
- params: props,
+ payload,
+ params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
diff --git a/frontend/src/api/infraMonitoring/getK8sPodsList.ts b/frontend/src/api/infraMonitoring/getK8sPodsList.ts
index 05258ef166b..560788285fc 100644
--- a/frontend/src/api/infraMonitoring/getK8sPodsList.ts
+++ b/frontend/src/api/infraMonitoring/getK8sPodsList.ts
@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { UnderscoreToDotMap } from '../utils';
+
export interface K8sPodsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -69,23 +71,87 @@ export interface K8sPodsListResponse {
};
}
+export const podsMetaMap = [
+ { dot: 'k8s.cronjob.name', under: 'k8s_cronjob_name' },
+ { dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
+ { dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
+ { dot: 'k8s.job.name', under: 'k8s_job_name' },
+ { dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
+ { dot: 'k8s.node.name', under: 'k8s_node_name' },
+ { dot: 'k8s.pod.name', under: 'k8s_pod_name' },
+ { dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
+ { dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
+ { dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
+] as const;
+
+export function mapPodsMeta(raw: Record): K8sPodsData['meta'] {
+ // clone everything
+ const out: Record = { ...raw };
+ // overlay only the dot→under mappings
+ podsMetaMap.forEach(({ dot, under }) => {
+ if (dot in raw) {
+ const v = raw[dot];
+ out[under] = typeof v === 'string' ? v : raw[under];
+ }
+ });
+ return out as K8sPodsData['meta'];
+}
+
+// getK8sPodsList
export const getK8sPodsList = async (
props: K8sPodsListPayload,
signal?: AbortSignal,
headers?: Record,
+ dotMetricsEnabled = false,
): Promise | ErrorResponse> => {
try {
- const response = await axios.post('/pods/list', props, {
+ const requestProps =
+ dotMetricsEnabled && Array.isArray(props.filters?.items)
+ ? {
+ ...props,
+ filters: {
+ ...props.filters,
+ items: props.filters.items.reduce(
+ (acc, item) => {
+ if (item.value === undefined) return acc;
+ if (
+ item.key &&
+ typeof item.key === 'object' &&
+ 'key' in item.key &&
+ typeof item.key.key === 'string'
+ ) {
+ const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
+ acc.push({
+ ...item,
+ key: { ...item.key, key: mappedKey },
+ });
+ } else {
+ acc.push(item);
+ }
+ return acc;
+ },
+ [] as typeof props.filters.items,
+ ),
+ },
+ }
+ : props;
+
+ const response = await axios.post('/pods/list', requestProps, {
signal,
headers,
});
+ const payload: K8sPodsListResponse = response.data;
+ payload.data.records = payload.data.records.map((record) => ({
+ ...record,
+ meta: mapPodsMeta(record.meta as Record),
+ }));
return {
statusCode: 200,
error: null,
message: 'Success',
- payload: response.data,
- params: props,
+ payload,
+ params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
diff --git a/frontend/src/api/infraMonitoring/getK8sVolumesList.ts b/frontend/src/api/infraMonitoring/getK8sVolumesList.ts
index ea825ba05d9..2089e5cd59f 100644
--- a/frontend/src/api/infraMonitoring/getK8sVolumesList.ts
+++ b/frontend/src/api/infraMonitoring/getK8sVolumesList.ts
@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { UnderscoreToDotMap } from '../utils';
+
export interface K8sVolumesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -47,23 +49,92 @@ export interface K8sVolumesListResponse {
};
}
+export const volumesMetaMap: Array<{
+ dot: keyof Record;
+ under: keyof K8sVolumesData['meta'];
+}> = [
+ { dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
+ { dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
+ { dot: 'k8s.node.name', under: 'k8s_node_name' },
+ {
+ dot: 'k8s.persistentvolumeclaim.name',
+ under: 'k8s_persistentvolumeclaim_name',
+ },
+ { dot: 'k8s.pod.name', under: 'k8s_pod_name' },
+ { dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
+ { dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
+];
+
+export function mapVolumesMeta(
+ rawMeta: Record,
+): K8sVolumesData['meta'] {
+ // start with everything that was already there
+ const out: Record = { ...rawMeta };
+
+ // for each dot→under rule, if the raw has the dot, overwrite the underscore
+ volumesMetaMap.forEach(({ dot, under }) => {
+ if (dot in rawMeta) {
+ const val = rawMeta[dot];
+ out[under] = typeof val === 'string' ? val : rawMeta[under];
+ }
+ });
+
+ return out as K8sVolumesData['meta'];
+}
+
export const getK8sVolumesList = async (
props: K8sVolumesListPayload,
signal?: AbortSignal,
headers?: Record,
+ dotMetricsEnabled = false,
): Promise | ErrorResponse> => {
try {
- const response = await axios.post('/pvcs/list', props, {
+ // Prepare filters
+ const requestProps =
+ dotMetricsEnabled && Array.isArray(props.filters?.items)
+ ? {
+ ...props,
+ filters: {
+ ...props.filters,
+ items: props.filters.items.reduce(
+ (acc, item) => {
+ if (item.value === undefined) return acc;
+ if (
+ item.key &&
+ typeof item.key === 'object' &&
+ 'key' in item.key &&
+ typeof item.key.key === 'string'
+ ) {
+ const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
+ acc.push({ ...item, key: { ...item.key, key: mappedKey } });
+ } else {
+ acc.push(item);
+ }
+ return acc;
+ },
+ [] as typeof props.filters.items,
+ ),
+ },
+ }
+ : props;
+
+ const response = await axios.post('/pvcs/list', requestProps, {
signal,
headers,
});
+ const payload: K8sVolumesListResponse = response.data;
+
+ payload.data.records = payload.data.records.map((record) => ({
+ ...record,
+ meta: mapVolumesMeta(record.meta as Record),
+ }));
return {
statusCode: 200,
error: null,
message: 'Success',
- payload: response.data,
- params: props,
+ payload,
+ params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
diff --git a/frontend/src/api/infraMonitoring/getsK8sStatefulSetsList.ts b/frontend/src/api/infraMonitoring/getsK8sStatefulSetsList.ts
index 191ec069c3d..2544c0cfe3b 100644
--- a/frontend/src/api/infraMonitoring/getsK8sStatefulSetsList.ts
+++ b/frontend/src/api/infraMonitoring/getsK8sStatefulSetsList.ts
@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { UnderscoreToDotMap } from '../utils';
+
export interface K8sStatefulSetsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -45,23 +47,78 @@ export interface K8sStatefulSetsListResponse {
};
}
+export const statefulSetsMetaMap = [
+ { dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
+ { dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
+] as const;
+
+export function mapStatefulSetsMeta(
+ raw: Record,
+): K8sStatefulSetsData['meta'] {
+ const out: Record = { ...raw };
+ statefulSetsMetaMap.forEach(({ dot, under }) => {
+ if (dot in raw) {
+ const v = raw[dot];
+ out[under] = typeof v === 'string' ? v : raw[under];
+ }
+ });
+ return out as K8sStatefulSetsData['meta'];
+}
+
export const getK8sStatefulSetsList = async (
props: K8sStatefulSetsListPayload,
signal?: AbortSignal,
headers?: Record,
+ dotMetricsEnabled = false,
): Promise | ErrorResponse> => {
try {
- const response = await axios.post('/statefulsets/list', props, {
+ // Prepare filters
+ const requestProps =
+ dotMetricsEnabled && Array.isArray(props.filters?.items)
+ ? {
+ ...props,
+ filters: {
+ ...props.filters,
+ items: props.filters.items.reduce(
+ (acc, item) => {
+ if (item.value === undefined) return acc;
+ if (
+ item.key &&
+ typeof item.key === 'object' &&
+ 'key' in item.key &&
+ typeof item.key.key === 'string'
+ ) {
+ const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
+ acc.push({ ...item, key: { ...item.key, key: mappedKey } });
+ } else {
+ acc.push(item);
+ }
+ return acc;
+ },
+ [] as typeof props.filters.items,
+ ),
+ },
+ }
+ : props;
+
+ const response = await axios.post('/statefulsets/list', requestProps, {
signal,
headers,
});
+ const payload: K8sStatefulSetsListResponse = response.data;
+
+ // apply our helper
+ payload.data.records = payload.data.records.map((record) => ({
+ ...record,
+ meta: mapStatefulSetsMeta(record.meta as Record),
+ }));
return {
statusCode: 200,
error: null,
message: 'Success',
- payload: response.data,
- params: props,
+ payload,
+ params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
diff --git a/frontend/src/api/licenses/apply.ts b/frontend/src/api/licenses/apply.ts
deleted file mode 100644
index c691ad836ff..00000000000
--- a/frontend/src/api/licenses/apply.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { ApiV3Instance as axios } from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/licenses/apply';
-
-const apply = async (
- props: Props,
-): Promise | ErrorResponse> => {
- try {
- const response = await axios.post('/licenses', {
- key: props.key,
- });
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default apply;
diff --git a/frontend/src/api/licenses/getAll.ts b/frontend/src/api/licenses/getAll.ts
deleted file mode 100644
index b05cdcb9e2c..00000000000
--- a/frontend/src/api/licenses/getAll.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { ApiV3Instance as axios } from 'api';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps } from 'types/api/licenses/getAll';
-
-const getAll = async (): Promise<
- SuccessResponse | ErrorResponse
-> => {
- const response = await axios.get('/licenses');
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
-};
-
-export default getAll;
diff --git a/frontend/src/api/licensesV3/getActive.ts b/frontend/src/api/licensesV3/getActive.ts
deleted file mode 100644
index 48dd0a3a434..00000000000
--- a/frontend/src/api/licensesV3/getActive.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { ApiV3Instance as axios } from 'api';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { LicenseV3EventQueueResModel } from 'types/api/licensesV3/getActive';
-
-const getActive = async (): Promise<
- SuccessResponse | ErrorResponse
-> => {
- const response = await axios.get('/licenses/active');
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
-};
-
-export default getActive;
diff --git a/frontend/src/api/metrics/getQueryRange.ts b/frontend/src/api/metrics/getQueryRange.ts
index 631372478df..d3ab626fa3d 100644
--- a/frontend/src/api/metrics/getQueryRange.ts
+++ b/frontend/src/api/metrics/getQueryRange.ts
@@ -2,7 +2,7 @@ import { ApiV3Instance, ApiV4Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ENTITY_VERSION_V4 } from 'constants/app';
-import { ErrorResponse, SuccessResponse } from 'types/api';
+import { ErrorResponse, SuccessResponse, Warning } from 'types/api';
import {
MetricRangePayloadV3,
QueryRangePayload,
@@ -13,7 +13,9 @@ export const getMetricsQueryRange = async (
version: string,
signal: AbortSignal,
headers?: Record,
-): Promise | ErrorResponse> => {
+): Promise<
+ (SuccessResponse & { warning?: Warning }) | ErrorResponse
+> => {
try {
if (version && version === ENTITY_VERSION_V4) {
const response = await ApiV4Instance.post('/query_range', props, {
diff --git a/frontend/src/api/metrics/getTopOperations.ts b/frontend/src/api/metrics/getTopOperations.ts
index 9c85602e40f..af02e2c333e 100644
--- a/frontend/src/api/metrics/getTopOperations.ts
+++ b/frontend/src/api/metrics/getTopOperations.ts
@@ -2,13 +2,20 @@ import axios from 'api';
import { PayloadProps, Props } from 'types/api/metrics/getTopOperations';
const getTopOperations = async (props: Props): Promise => {
- const response = await axios.post(`/service/top_operations`, {
+ const endpoint = props.isEntryPoint
+ ? '/service/entry_point_operations'
+ : '/service/top_operations';
+
+ const response = await axios.post(endpoint, {
start: `${props.start}`,
end: `${props.end}`,
service: props.service,
tags: props.selectedTags,
});
+ if (props.isEntryPoint) {
+ return response.data.data;
+ }
return response.data;
};
diff --git a/frontend/src/api/pipeline/post.ts b/frontend/src/api/pipeline/post.ts
index c2e7ca27570..9c774481f6d 100644
--- a/frontend/src/api/pipeline/post.ts
+++ b/frontend/src/api/pipeline/post.ts
@@ -1,24 +1,20 @@
import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Pipeline } from 'types/api/pipeline/def';
import { Props } from 'types/api/pipeline/post';
-const post = async (
- props: Props,
-): Promise | ErrorResponse> => {
+const post = async (props: Props): Promise> => {
try {
const response = await axios.post('/logs/pipelines', props.data);
return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
+ httpStatusCode: response.status,
+ data: response.data.data,
};
} catch (error) {
- return ErrorResponseHandler(error as AxiosError);
+ ErrorResponseHandlerV2(error as AxiosError);
}
};
diff --git a/frontend/src/api/preferences/getAllOrgPreferences.ts b/frontend/src/api/preferences/getAllOrgPreferences.ts
deleted file mode 100644
index 12f6497c0eb..00000000000
--- a/frontend/src/api/preferences/getAllOrgPreferences.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import axios from 'api';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
-
-const getAllOrgPreferences = async (): Promise<
- SuccessResponse | ErrorResponse
-> => {
- const response = await axios.get(`/org/preferences`);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data,
- };
-};
-
-export default getAllOrgPreferences;
diff --git a/frontend/src/api/preferences/getAllUserPreference.ts b/frontend/src/api/preferences/getAllUserPreference.ts
deleted file mode 100644
index f7e94f76cb7..00000000000
--- a/frontend/src/api/preferences/getAllUserPreference.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import axios from 'api';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
-
-const getAllUserPreferences = async (): Promise<
- SuccessResponse | ErrorResponse
-> => {
- const response = await axios.get(`/user/preferences`);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data,
- };
-};
-
-export default getAllUserPreferences;
diff --git a/frontend/src/api/preferences/getOrgPreference.ts b/frontend/src/api/preferences/getOrgPreference.ts
deleted file mode 100644
index 6a016ddd3a7..00000000000
--- a/frontend/src/api/preferences/getOrgPreference.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import axios from 'api';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
-
-const getOrgPreference = async ({
- preferenceID,
-}: {
- preferenceID: string;
-}): Promise | ErrorResponse> => {
- const response = await axios.get(`/org/preferences/${preferenceID}`);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data,
- };
-};
-
-export default getOrgPreference;
diff --git a/frontend/src/api/preferences/getUserPreference.ts b/frontend/src/api/preferences/getUserPreference.ts
deleted file mode 100644
index a750732a925..00000000000
--- a/frontend/src/api/preferences/getUserPreference.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import axios from 'api';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
-
-const getUserPreference = async ({
- preferenceID,
-}: {
- preferenceID: string;
-}): Promise<
- SuccessResponse | ErrorResponse
-> => {
- const response = await axios.get(`/user/preferences/${preferenceID}`);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data,
- };
-};
-
-export default getUserPreference;
diff --git a/frontend/src/api/preferences/updateOrgPreference.ts b/frontend/src/api/preferences/updateOrgPreference.ts
deleted file mode 100644
index aae4d83ddf0..00000000000
--- a/frontend/src/api/preferences/updateOrgPreference.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import axios from 'api';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import {
- UpdateOrgPreferenceProps,
- UpdateOrgPreferenceResponseProps,
-} from 'types/api/preferences/userOrgPreferences';
-
-const updateOrgPreference = async (
- preferencePayload: UpdateOrgPreferenceProps,
-): Promise<
- SuccessResponse | ErrorResponse
-> => {
- const response = await axios.put(
- `/org/preferences/${preferencePayload.preferenceID}`,
- {
- preference_value: preferencePayload.value,
- },
- );
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
-};
-
-export default updateOrgPreference;
diff --git a/frontend/src/api/preferences/updateUserPreference.ts b/frontend/src/api/preferences/updateUserPreference.ts
deleted file mode 100644
index 3cf215993fb..00000000000
--- a/frontend/src/api/preferences/updateUserPreference.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import axios from 'api';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import {
- UpdateUserPreferenceProps,
- UpdateUserPreferenceResponseProps,
-} from 'types/api/preferences/userOrgPreferences';
-
-const updateUserPreference = async (
- preferencePayload: UpdateUserPreferenceProps,
-): Promise<
- SuccessResponse | ErrorResponse
-> => {
- const response = await axios.put(
- `/user/preferences/${preferencePayload.preferenceID}`,
- {
- preference_value: preferencePayload.value,
- },
- );
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data.data,
- };
-};
-
-export default updateUserPreference;
diff --git a/frontend/src/api/queryBuilder/getAggregateAttribute.ts b/frontend/src/api/queryBuilder/getAggregateAttribute.ts
index f13c3da4a89..8080d9cc6f9 100644
--- a/frontend/src/api/queryBuilder/getAggregateAttribute.ts
+++ b/frontend/src/api/queryBuilder/getAggregateAttribute.ts
@@ -17,6 +17,7 @@ export const getAggregateAttribute = async ({
aggregateOperator,
searchText,
dataSource,
+ source,
}: IGetAggregateAttributePayload): Promise<
SuccessResponse | ErrorResponse
> => {
@@ -27,7 +28,7 @@ export const getAggregateAttribute = async ({
`/autocomplete/aggregate_attributes?${createQueryParams({
aggregateOperator,
searchText,
- dataSource,
+ dataSource: source === 'meter' ? 'meter' : dataSource,
})}`,
);
diff --git a/frontend/src/api/querySuggestions/getKeySuggestions.ts b/frontend/src/api/querySuggestions/getKeySuggestions.ts
new file mode 100644
index 00000000000..50626dee8b8
--- /dev/null
+++ b/frontend/src/api/querySuggestions/getKeySuggestions.ts
@@ -0,0 +1,30 @@
+import axios from 'api';
+import { AxiosResponse } from 'axios';
+import {
+ QueryKeyRequestProps,
+ QueryKeySuggestionsResponseProps,
+} from 'types/api/querySuggestions/types';
+
+export const getKeySuggestions = (
+ props: QueryKeyRequestProps,
+): Promise> => {
+ const {
+ signal = '',
+ searchText = '',
+ metricName = '',
+ fieldContext = '',
+ fieldDataType = '',
+ signalSource = '',
+ } = props;
+
+ const encodedSignal = encodeURIComponent(signal);
+ const encodedSearchText = encodeURIComponent(searchText);
+ const encodedMetricName = encodeURIComponent(metricName);
+ const encodedFieldContext = encodeURIComponent(fieldContext);
+ const encodedFieldDataType = encodeURIComponent(fieldDataType);
+ const encodedSource = encodeURIComponent(signalSource);
+
+ return axios.get(
+ `/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
+ );
+};
diff --git a/frontend/src/api/querySuggestions/getValueSuggestion.ts b/frontend/src/api/querySuggestions/getValueSuggestion.ts
new file mode 100644
index 00000000000..f91a40139b9
--- /dev/null
+++ b/frontend/src/api/querySuggestions/getValueSuggestion.ts
@@ -0,0 +1,22 @@
+import axios from 'api';
+import { AxiosResponse } from 'axios';
+import {
+ QueryKeyValueRequestProps,
+ QueryKeyValueSuggestionsResponseProps,
+} from 'types/api/querySuggestions/types';
+
+export const getValueSuggestions = (
+ props: QueryKeyValueRequestProps,
+): Promise> => {
+ const { signal, key, searchText, signalSource, metricName } = props;
+
+ const encodedSignal = encodeURIComponent(signal);
+ const encodedKey = encodeURIComponent(key);
+ const encodedMetricName = encodeURIComponent(metricName || '');
+ const encodedSearchText = encodeURIComponent(searchText);
+ const encodedSource = encodeURIComponent(signalSource || '');
+
+ return axios.get(
+ `/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`,
+ );
+};
diff --git a/frontend/src/api/routingPolicies/createRoutingPolicy.ts b/frontend/src/api/routingPolicies/createRoutingPolicy.ts
new file mode 100644
index 00000000000..69bf58bd8c8
--- /dev/null
+++ b/frontend/src/api/routingPolicies/createRoutingPolicy.ts
@@ -0,0 +1,34 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+
+export interface CreateRoutingPolicyBody {
+ name: string;
+ expression: string;
+ channels: string[];
+ description?: string;
+}
+
+export interface CreateRoutingPolicyResponse {
+ success: boolean;
+ message: string;
+}
+
+const createRoutingPolicy = async (
+ props: CreateRoutingPolicyBody,
+): Promise<
+ SuccessResponseV2 | ErrorResponseV2
+> => {
+ try {
+ const response = await axios.post(`/route_policies`, props);
+ return {
+ httpStatusCode: response.status,
+ data: response.data,
+ };
+ } catch (error) {
+ return ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default createRoutingPolicy;
diff --git a/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts b/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts
new file mode 100644
index 00000000000..444ec0b1ab9
--- /dev/null
+++ b/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts
@@ -0,0 +1,28 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+
+export interface DeleteRoutingPolicyResponse {
+ success: boolean;
+ message: string;
+}
+
+const deleteRoutingPolicy = async (
+ routingPolicyId: string,
+): Promise<
+ SuccessResponseV2 | ErrorResponseV2
+> => {
+ try {
+ const response = await axios.delete(`/route_policies/${routingPolicyId}`);
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data,
+ };
+ } catch (error) {
+ return ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default deleteRoutingPolicy;
diff --git a/frontend/src/api/routingPolicies/getRoutingPolicies.ts b/frontend/src/api/routingPolicies/getRoutingPolicies.ts
new file mode 100644
index 00000000000..b06d359c9d8
--- /dev/null
+++ b/frontend/src/api/routingPolicies/getRoutingPolicies.ts
@@ -0,0 +1,40 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+
+export interface ApiRoutingPolicy {
+ id: string;
+ name: string;
+ expression: string;
+ description: string;
+ channels: string[];
+ createdAt: string;
+ updatedAt: string;
+ createdBy: string;
+ updatedBy: string;
+}
+
+export interface GetRoutingPoliciesResponse {
+ status: string;
+ data?: ApiRoutingPolicy[];
+}
+
+export const getRoutingPolicies = async (
+ signal?: AbortSignal,
+ headers?: Record,
+): Promise | ErrorResponseV2> => {
+ try {
+ const response = await axios.get('/route_policies', {
+ signal,
+ headers,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data,
+ };
+ } catch (error) {
+ return ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
diff --git a/frontend/src/api/routingPolicies/updateRoutingPolicy.ts b/frontend/src/api/routingPolicies/updateRoutingPolicy.ts
new file mode 100644
index 00000000000..63731fe5bf9
--- /dev/null
+++ b/frontend/src/api/routingPolicies/updateRoutingPolicy.ts
@@ -0,0 +1,38 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+
+export interface UpdateRoutingPolicyBody {
+ name: string;
+ expression: string;
+ channels: string[];
+ description: string;
+}
+
+export interface UpdateRoutingPolicyResponse {
+ success: boolean;
+ message: string;
+}
+
+const updateRoutingPolicy = async (
+ id: string,
+ props: UpdateRoutingPolicyBody,
+): Promise<
+ SuccessResponseV2 | ErrorResponseV2
+> => {
+ try {
+ const response = await axios.put(`/route_policies/${id}`, {
+ ...props,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data,
+ };
+ } catch (error) {
+ return ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default updateRoutingPolicy;
diff --git a/frontend/src/api/saveView/getAllViews.ts b/frontend/src/api/saveView/getAllViews.ts
index 4a54d6af0df..a26fd13441f 100644
--- a/frontend/src/api/saveView/getAllViews.ts
+++ b/frontend/src/api/saveView/getAllViews.ts
@@ -4,6 +4,6 @@ import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = (
- sourcepage: DataSource,
+ sourcepage: DataSource | 'meter',
): Promise> =>
axios.get(`/explorer/views?sourcePage=${sourcepage}`);
diff --git a/frontend/src/api/settings/getRetentionV2.ts b/frontend/src/api/settings/getRetentionV2.ts
new file mode 100644
index 00000000000..8fa5ab1d851
--- /dev/null
+++ b/frontend/src/api/settings/getRetentionV2.ts
@@ -0,0 +1,25 @@
+import { ApiV2Instance } from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps } from 'types/api/settings/getRetention';
+
+// Only works for logs
+const getRetentionV2 = async (): Promise<
+ SuccessResponseV2>
+> => {
+ try {
+ const response = await ApiV2Instance.get>(
+ `/settings/ttl`,
+ );
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default getRetentionV2;
diff --git a/frontend/src/api/settings/setRetention.ts b/frontend/src/api/settings/setRetention.ts
index 481760bf57b..3ff38e7319d 100644
--- a/frontend/src/api/settings/setRetention.ts
+++ b/frontend/src/api/settings/setRetention.ts
@@ -1,14 +1,14 @@
import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/settings/setRetention';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadPropsV2, Props } from 'types/api/settings/setRetention';
const setRetention = async (
props: Props,
-): Promise | ErrorResponse> => {
+): Promise> => {
try {
- const response = await axios.post(
+ const response = await axios.post(
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
props.coldStorage
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
@@ -17,13 +17,11 @@ const setRetention = async (
);
return {
- statusCode: 200,
- error: null,
- message: 'Success',
- payload: response.data,
+ httpStatusCode: response.status,
+ data: response.data,
};
} catch (error) {
- return ErrorResponseHandler(error as AxiosError);
+ ErrorResponseHandlerV2(error as AxiosError);
}
};
diff --git a/frontend/src/api/settings/setRetentionV2.ts b/frontend/src/api/settings/setRetentionV2.ts
new file mode 100644
index 00000000000..bd7e56d8e5f
--- /dev/null
+++ b/frontend/src/api/settings/setRetentionV2.ts
@@ -0,0 +1,32 @@
+import { ApiV2Instance } from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadPropsV2, PropsV2 } from 'types/api/settings/setRetention';
+
+const setRetentionV2 = async ({
+ type,
+ defaultTTLDays,
+ coldStorageVolume,
+ coldStorageDuration,
+ ttlConditions,
+}: PropsV2): Promise> => {
+ try {
+ const response = await ApiV2Instance.post(`/settings/ttl`, {
+ type,
+ defaultTTLDays,
+ coldStorageVolume,
+ coldStorageDuration,
+ ttlConditions,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default setRetentionV2;
diff --git a/frontend/src/api/thirdPartyApis/listOverview.ts b/frontend/src/api/thirdPartyApis/listOverview.ts
new file mode 100644
index 00000000000..303ee03ae06
--- /dev/null
+++ b/frontend/src/api/thirdPartyApis/listOverview.ts
@@ -0,0 +1,31 @@
+import { ApiBaseInstance } from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps, Props } from 'types/api/thirdPartyApis/listOverview';
+
+const listOverview = async (
+ props: Props,
+): Promise> => {
+ const { start, end, show_ip: showIp, filter } = props;
+ try {
+ const response = await ApiBaseInstance.post(
+ `/third-party-apis/overview/list`,
+ {
+ start,
+ end,
+ show_ip: showIp,
+ filter,
+ },
+ );
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default listOverview;
diff --git a/frontend/src/api/traceFunnels/index.ts b/frontend/src/api/traceFunnels/index.ts
index d4c3b8ccca8..63623eccaaf 100644
--- a/frontend/src/api/traceFunnels/index.ts
+++ b/frontend/src/api/traceFunnels/index.ts
@@ -119,6 +119,7 @@ export const updateFunnelSteps = async (
export interface ValidateFunnelPayload {
start_time: number;
end_time: number;
+ steps: FunnelStepData[];
}
export interface ValidateFunnelResponse {
@@ -132,12 +133,11 @@ export interface ValidateFunnelResponse {
}
export const validateFunnelSteps = async (
- funnelId: string,
payload: ValidateFunnelPayload,
signal?: AbortSignal,
): Promise | ErrorResponse> => {
const response = await axios.post(
- `${FUNNELS_BASE_PATH}/${funnelId}/analytics/validate`,
+ `${FUNNELS_BASE_PATH}/analytics/validate`,
payload,
{ signal },
);
@@ -167,8 +167,8 @@ interface UpdateFunnelDescriptionPayload {
export const saveFunnelDescription = async (
payload: UpdateFunnelDescriptionPayload,
): Promise | ErrorResponse> => {
- const response: AxiosResponse = await axios.post(
- `${FUNNELS_BASE_PATH}/save`,
+ const response: AxiosResponse = await axios.put(
+ `${FUNNELS_BASE_PATH}/${payload.funnel_id}`,
payload,
);
@@ -185,6 +185,7 @@ export interface FunnelOverviewPayload {
end_time: number;
step_start?: number;
step_end?: number;
+ steps: FunnelStepData[];
}
export interface FunnelOverviewResponse {
@@ -196,20 +197,17 @@ export interface FunnelOverviewResponse {
avg_rate: number;
conversion_rate: number | null;
errors: number;
- // TODO(shaheer): remove p99_latency once we have support for latency
- p99_latency: number;
latency: number;
};
}>;
}
export const getFunnelOverview = async (
- funnelId: string,
payload: FunnelOverviewPayload,
signal?: AbortSignal,
): Promise | ErrorResponse> => {
const response = await axios.post(
- `${FUNNELS_BASE_PATH}/${funnelId}/analytics/overview`,
+ `${FUNNELS_BASE_PATH}/analytics/overview`,
payload,
{
signal,
@@ -237,12 +235,11 @@ export interface SlowTraceData {
}
export const getFunnelSlowTraces = async (
- funnelId: string,
payload: FunnelOverviewPayload,
signal?: AbortSignal,
): Promise | ErrorResponse> => {
const response = await axios.post(
- `${FUNNELS_BASE_PATH}/${funnelId}/analytics/slow-traces`,
+ `${FUNNELS_BASE_PATH}/analytics/slow-traces`,
payload,
{
signal,
@@ -275,7 +272,7 @@ export const getFunnelErrorTraces = async (
signal?: AbortSignal,
): Promise | ErrorResponse> => {
const response: AxiosResponse = await axios.post(
- `${FUNNELS_BASE_PATH}/${funnelId}/analytics/error-traces`,
+ `${FUNNELS_BASE_PATH}/analytics/error-traces`,
payload,
{
signal,
@@ -293,6 +290,7 @@ export const getFunnelErrorTraces = async (
export interface FunnelStepsPayload {
start_time: number;
end_time: number;
+ steps: FunnelStepData[];
}
export interface FunnelStepGraphMetrics {
@@ -309,12 +307,11 @@ export interface FunnelStepsResponse {
}
export const getFunnelSteps = async (
- funnelId: string,
payload: FunnelStepsPayload,
signal?: AbortSignal,
): Promise | ErrorResponse> => {
const response = await axios.post(
- `${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps`,
+ `${FUNNELS_BASE_PATH}/analytics/steps`,
payload,
{ signal },
);
@@ -332,6 +329,7 @@ export interface FunnelStepsOverviewPayload {
end_time: number;
step_start?: number;
step_end?: number;
+ steps: FunnelStepData[];
}
export interface FunnelStepsOverviewResponse {
@@ -343,12 +341,11 @@ export interface FunnelStepsOverviewResponse {
}
export const getFunnelStepsOverview = async (
- funnelId: string,
payload: FunnelStepsOverviewPayload,
signal?: AbortSignal,
): Promise | ErrorResponse> => {
const response = await axios.post(
- `${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps/overview`,
+ `${FUNNELS_BASE_PATH}/analytics/steps/overview`,
payload,
{ signal },
);
diff --git a/frontend/src/api/userFeedback/sendFeedback.ts b/frontend/src/api/userFeedback/sendFeedback.ts
deleted file mode 100644
index abf811378c2..00000000000
--- a/frontend/src/api/userFeedback/sendFeedback.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import axios from 'api';
-import { Props } from 'types/api/userFeedback/sendResponse';
-
-const sendFeedback = async (props: Props): Promise => {
- const response = await axios.post(
- '/feedback',
- {
- email: props.email,
- message: props.message,
- },
- {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- },
- );
-
- return response.status;
-};
-
-export default sendFeedback;
diff --git a/frontend/src/api/utils.ts b/frontend/src/api/utils.ts
index 179eac8e21e..731b8e859af 100644
--- a/frontend/src/api/utils.ts
+++ b/frontend/src/api/utils.ts
@@ -15,13 +15,21 @@ export const Logout = (): void => {
deleteLocalStorageKey(LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT);
window.dispatchEvent(new CustomEvent('LOGOUT'));
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- if (window && window.Intercom) {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- window.Intercom('shutdown');
- }
-
history.push(ROUTES.LOGIN);
};
+
+export const UnderscoreToDotMap: Record = {
+ k8s_cluster_name: 'k8s.cluster.name',
+ k8s_cluster_uid: 'k8s.cluster.uid',
+ k8s_namespace_name: 'k8s.namespace.name',
+ k8s_node_name: 'k8s.node.name',
+ k8s_node_uid: 'k8s.node.uid',
+ k8s_pod_name: 'k8s.pod.name',
+ k8s_pod_uid: 'k8s.pod.uid',
+ k8s_deployment_name: 'k8s.deployment.name',
+ k8s_daemonset_name: 'k8s.daemonset.name',
+ k8s_statefulset_name: 'k8s.statefulset.name',
+ k8s_cronjob_name: 'k8s.cronjob.name',
+ k8s_job_name: 'k8s.job.name',
+ k8s_persistentvolumeclaim_name: 'k8s.persistentvolumeclaim.name',
+};
diff --git a/frontend/src/api/v1/checkout/create.ts b/frontend/src/api/v1/checkout/create.ts
new file mode 100644
index 00000000000..2e71a647686
--- /dev/null
+++ b/frontend/src/api/v1/checkout/create.ts
@@ -0,0 +1,28 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import {
+ CheckoutRequestPayloadProps,
+ CheckoutSuccessPayloadProps,
+ PayloadProps,
+} from 'types/api/billing/checkout';
+
+const updateCreditCardApi = async (
+ props: CheckoutRequestPayloadProps,
+): Promise> => {
+ try {
+ const response = await axios.post('/checkout', {
+ url: props.url,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default updateCreditCardApi;
diff --git a/frontend/src/api/v1/dashboards/create.ts b/frontend/src/api/v1/dashboards/create.ts
new file mode 100644
index 00000000000..1eb7bdc12e3
--- /dev/null
+++ b/frontend/src/api/v1/dashboards/create.ts
@@ -0,0 +1,23 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps, Props } from 'types/api/dashboard/create';
+import { Dashboard } from 'types/api/dashboard/getAll';
+
+const create = async (props: Props): Promise> => {
+ try {
+ const response = await axios.post('/dashboards', {
+ ...props,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default create;
diff --git a/frontend/src/api/v1/dashboards/getAll.ts b/frontend/src/api/v1/dashboards/getAll.ts
new file mode 100644
index 00000000000..7d89ef94f03
--- /dev/null
+++ b/frontend/src/api/v1/dashboards/getAll.ts
@@ -0,0 +1,19 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll';
+
+const getAll = async (): Promise> => {
+ try {
+ const response = await axios.get('/dashboards');
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default getAll;
diff --git a/frontend/src/api/v1/dashboards/id/delete.ts b/frontend/src/api/v1/dashboards/id/delete.ts
new file mode 100644
index 00000000000..e6c974df026
--- /dev/null
+++ b/frontend/src/api/v1/dashboards/id/delete.ts
@@ -0,0 +1,21 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps, Props } from 'types/api/dashboard/delete';
+
+const deleteDashboard = async (
+ props: Props,
+): Promise> => {
+ try {
+ const response = await axios.delete(`/dashboards/${props.id}`);
+ return {
+ httpStatusCode: response.status,
+ data: null,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default deleteDashboard;
diff --git a/frontend/src/api/v1/dashboards/id/get.ts b/frontend/src/api/v1/dashboards/id/get.ts
new file mode 100644
index 00000000000..dfde573342e
--- /dev/null
+++ b/frontend/src/api/v1/dashboards/id/get.ts
@@ -0,0 +1,20 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps, Props } from 'types/api/dashboard/get';
+import { Dashboard } from 'types/api/dashboard/getAll';
+
+const get = async (props: Props): Promise> => {
+ try {
+ const response = await axios.get(`/dashboards/${props.id}`);
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default get;
diff --git a/frontend/src/api/v1/dashboards/id/lock.ts b/frontend/src/api/v1/dashboards/id/lock.ts
new file mode 100644
index 00000000000..289a4ddc990
--- /dev/null
+++ b/frontend/src/api/v1/dashboards/id/lock.ts
@@ -0,0 +1,23 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps, Props } from 'types/api/dashboard/lockUnlock';
+
+const lock = async (props: Props): Promise> => {
+ try {
+ const response = await axios.put(
+ `/dashboards/${props.id}/lock`,
+ { lock: props.lock },
+ );
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default lock;
diff --git a/frontend/src/api/v1/dashboards/id/update.ts b/frontend/src/api/v1/dashboards/id/update.ts
new file mode 100644
index 00000000000..82e98039bca
--- /dev/null
+++ b/frontend/src/api/v1/dashboards/id/update.ts
@@ -0,0 +1,23 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { Dashboard } from 'types/api/dashboard/getAll';
+import { PayloadProps, Props } from 'types/api/dashboard/update';
+
+const update = async (props: Props): Promise> => {
+ try {
+ const response = await axios.put(`/dashboards/${props.id}`, {
+ ...props.data,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default update;
diff --git a/frontend/src/api/v1/domains/create.ts b/frontend/src/api/v1/domains/create.ts
new file mode 100644
index 00000000000..18fbc21b2bd
--- /dev/null
+++ b/frontend/src/api/v1/domains/create.ts
@@ -0,0 +1,21 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { AuthDomain } from 'types/api/SAML/listDomain';
+import { PayloadProps, Props } from 'types/api/SAML/postDomain';
+
+const create = async (props: Props): Promise> => {
+ try {
+ const response = await axios.post(`/domains`, props);
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default create;
diff --git a/frontend/src/api/v1/domains/delete.ts b/frontend/src/api/v1/domains/delete.ts
new file mode 100644
index 00000000000..0c1f452248f
--- /dev/null
+++ b/frontend/src/api/v1/domains/delete.ts
@@ -0,0 +1,20 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps, Props } from 'types/api/SAML/deleteDomain';
+
+const deleteDomain = async (props: Props): Promise> => {
+ try {
+ const response = await axios.delete(`/domains/${props.id}`);
+
+ return {
+ httpStatusCode: response.status,
+ data: null,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default deleteDomain;
diff --git a/frontend/src/api/v1/domains/list.ts b/frontend/src/api/v1/domains/list.ts
new file mode 100644
index 00000000000..fc056873a06
--- /dev/null
+++ b/frontend/src/api/v1/domains/list.ts
@@ -0,0 +1,20 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { AuthDomain, PayloadProps } from 'types/api/SAML/listDomain';
+
+const listAllDomain = async (): Promise> => {
+ try {
+ const response = await axios.get(`/domains`);
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default listAllDomain;
diff --git a/frontend/src/api/v1/domains/update.ts b/frontend/src/api/v1/domains/update.ts
new file mode 100644
index 00000000000..701555a39d1
--- /dev/null
+++ b/frontend/src/api/v1/domains/update.ts
@@ -0,0 +1,23 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { AuthDomain } from 'types/api/SAML/listDomain';
+import { PayloadProps, Props } from 'types/api/SAML/updateDomain';
+
+const updateDomain = async (
+ props: Props,
+): Promise> => {
+ try {
+ const response = await axios.put(`/domains/${props.id}`, props);
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default updateDomain;
diff --git a/frontend/src/api/v1/download/downloadExportData.ts b/frontend/src/api/v1/download/downloadExportData.ts
new file mode 100644
index 00000000000..30bc7b25dd7
--- /dev/null
+++ b/frontend/src/api/v1/download/downloadExportData.ts
@@ -0,0 +1,64 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp } from 'types/api';
+import { ExportRawDataProps } from 'types/api/exportRawData/getExportRawData';
+
+export const downloadExportData = async (
+ props: ExportRawDataProps,
+): Promise => {
+ try {
+ const queryParams = new URLSearchParams();
+
+ queryParams.append('start', String(props.start));
+ queryParams.append('end', String(props.end));
+ queryParams.append('filter', props.filter);
+ props.columns.forEach((col) => {
+ queryParams.append('columns', col);
+ });
+ queryParams.append('order_by', props.orderBy);
+ queryParams.append('limit', String(props.limit));
+ queryParams.append('format', props.format);
+
+ const response = await axios.get(`export_raw_data?${queryParams}`, {
+ responseType: 'blob', // Important: tell axios to handle response as blob
+ decompress: true, // Enable automatic decompression
+ headers: {
+ Accept: 'application/octet-stream', // Tell server we expect binary data
+ },
+ timeout: 0,
+ });
+
+ // Only proceed if the response status is 200
+ if (response.status !== 200) {
+ throw new Error(
+ `Failed to download data: server returned status ${response.status}`,
+ );
+ }
+ // Create blob URL from response data
+ const blob = new Blob([response.data], { type: 'application/octet-stream' });
+ const url = window.URL.createObjectURL(blob);
+
+ // Create and configure download link
+ const link = document.createElement('a');
+ link.href = url;
+
+ // Get filename from Content-Disposition header or generate timestamped default
+ const filename =
+ response.headers['content-disposition']
+ ?.split('filename=')[1]
+ ?.replace(/["']/g, '') || `exported_data.${props.format || 'txt'}`;
+
+ link.setAttribute('download', filename);
+
+ // Trigger download
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ URL.revokeObjectURL(url);
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default downloadExportData;
diff --git a/frontend/src/api/v1/features/list.ts b/frontend/src/api/v1/features/list.ts
new file mode 100644
index 00000000000..43a445ead5d
--- /dev/null
+++ b/frontend/src/api/v1/features/list.ts
@@ -0,0 +1,23 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import {
+ FeatureFlagProps,
+ PayloadProps,
+} from 'types/api/features/getFeaturesFlags';
+
+const list = async (): Promise> => {
+ try {
+ const response = await axios.get(`/features`);
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default list;
diff --git a/frontend/src/api/v1/login/loginPrecheck.ts b/frontend/src/api/v1/login/loginPrecheck.ts
index c0cdc3dcc43..eac00182cb5 100644
--- a/frontend/src/api/v1/login/loginPrecheck.ts
+++ b/frontend/src/api/v1/login/loginPrecheck.ts
@@ -2,7 +2,7 @@ import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps, Props } from 'types/api/user/loginPrecheck';
+import { Props, Signup as PayloadProps } from 'types/api/user/loginPrecheck';
const loginPrecheck = async (
props: Props,
diff --git a/frontend/src/api/v1/org/preferences/list.ts b/frontend/src/api/v1/org/preferences/list.ts
new file mode 100644
index 00000000000..6864639908d
--- /dev/null
+++ b/frontend/src/api/v1/org/preferences/list.ts
@@ -0,0 +1,23 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps } from 'types/api/preferences/list';
+import { OrgPreference } from 'types/api/preferences/preference';
+
+const listPreference = async (): Promise<
+ SuccessResponseV2
+> => {
+ try {
+ const response = await axios.get(`/org/preferences`);
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default listPreference;
diff --git a/frontend/src/api/v1/org/preferences/name/get.ts b/frontend/src/api/v1/org/preferences/name/get.ts
new file mode 100644
index 00000000000..48e746d2a1b
--- /dev/null
+++ b/frontend/src/api/v1/org/preferences/name/get.ts
@@ -0,0 +1,25 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps, Props } from 'types/api/preferences/get';
+import { OrgPreference } from 'types/api/preferences/preference';
+
+const getPreference = async (
+ props: Props,
+): Promise> => {
+ try {
+ const response = await axios.get(
+ `/org/preferences/${props.name}`,
+ );
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default getPreference;
diff --git a/frontend/src/api/v1/org/preferences/name/update.ts b/frontend/src/api/v1/org/preferences/name/update.ts
new file mode 100644
index 00000000000..d226c9b96c6
--- /dev/null
+++ b/frontend/src/api/v1/org/preferences/name/update.ts
@@ -0,0 +1,22 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { Props } from 'types/api/preferences/update';
+
+const update = async (props: Props): Promise> => {
+ try {
+ const response = await axios.put(`/org/preferences/${props.name}`, {
+ value: props.value,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: null,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default update;
diff --git a/frontend/src/api/v1/pats/create.ts b/frontend/src/api/v1/pats/create.ts
new file mode 100644
index 00000000000..c487a91bb34
--- /dev/null
+++ b/frontend/src/api/v1/pats/create.ts
@@ -0,0 +1,28 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import {
+ APIKeyProps,
+ CreateAPIKeyProps,
+ CreatePayloadProps,
+} from 'types/api/pat/types';
+
+const create = async (
+ props: CreateAPIKeyProps,
+): Promise> => {
+ try {
+ const response = await axios.post('/pats', {
+ ...props,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default create;
diff --git a/frontend/src/api/v1/pats/delete.ts b/frontend/src/api/v1/pats/delete.ts
new file mode 100644
index 00000000000..716bdffc196
--- /dev/null
+++ b/frontend/src/api/v1/pats/delete.ts
@@ -0,0 +1,19 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+
+const deleteAPIKey = async (id: string): Promise> => {
+ try {
+ const response = await axios.delete(`/pats/${id}`);
+
+ return {
+ httpStatusCode: response.status,
+ data: null,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default deleteAPIKey;
diff --git a/frontend/src/api/v1/pats/list.ts b/frontend/src/api/v1/pats/list.ts
new file mode 100644
index 00000000000..bc4833af51d
--- /dev/null
+++ b/frontend/src/api/v1/pats/list.ts
@@ -0,0 +1,20 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { AllAPIKeyProps, APIKeyProps } from 'types/api/pat/types';
+
+const list = async (): Promise> => {
+ try {
+ const response = await axios.get('/pats');
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default list;
diff --git a/frontend/src/api/v1/pats/update.ts b/frontend/src/api/v1/pats/update.ts
new file mode 100644
index 00000000000..b7c1e016c8f
--- /dev/null
+++ b/frontend/src/api/v1/pats/update.ts
@@ -0,0 +1,24 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { UpdateAPIKeyProps } from 'types/api/pat/types';
+
+const updateAPIKey = async (
+ props: UpdateAPIKeyProps,
+): Promise> => {
+ try {
+ const response = await axios.put(`/pats/${props.id}`, {
+ ...props.data,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: null,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default updateAPIKey;
diff --git a/frontend/src/api/v1/portal/create.ts b/frontend/src/api/v1/portal/create.ts
new file mode 100644
index 00000000000..1c6854ffe29
--- /dev/null
+++ b/frontend/src/api/v1/portal/create.ts
@@ -0,0 +1,28 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import {
+ CheckoutRequestPayloadProps,
+ CheckoutSuccessPayloadProps,
+ PayloadProps,
+} from 'types/api/billing/checkout';
+
+const manageCreditCardApi = async (
+ props: CheckoutRequestPayloadProps,
+): Promise> => {
+ try {
+ const response = await axios.post('/portal', {
+ url: props.url,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default manageCreditCardApi;
diff --git a/frontend/src/api/v1/register/signup.ts b/frontend/src/api/v1/register/signup.ts
index fcb483dffba..5838a8e7adf 100644
--- a/frontend/src/api/v1/register/signup.ts
+++ b/frontend/src/api/v1/register/signup.ts
@@ -1,25 +1,21 @@
import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps } from 'types/api/user/loginPrecheck';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps, Signup } from 'types/api/user/loginPrecheck';
import { Props } from 'types/api/user/signup';
-const signup = async (
- props: Props,
-): Promise | ErrorResponse> => {
+const signup = async (props: Props): Promise> => {
try {
- const response = await axios.post(`/register`, {
+ const response = await axios.post(`/register`, {
...props,
});
return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data?.data,
+ httpStatusCode: response.status,
+ data: response.data.data,
};
} catch (error) {
- return ErrorResponseHandler(error as AxiosError);
+ ErrorResponseHandlerV2(error as AxiosError);
}
};
diff --git a/frontend/src/api/v1/user/preferences/get.ts b/frontend/src/api/v1/user/preferences/get.ts
deleted file mode 100644
index 5a29113fa44..00000000000
--- a/frontend/src/api/v1/user/preferences/get.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import axios from 'api';
-import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
-import { AxiosError } from 'axios';
-import { ErrorResponse, SuccessResponse } from 'types/api';
-import { PayloadProps } from 'types/api/user/getUserPreference';
-
-const getPreference = async (): Promise<
- SuccessResponse | ErrorResponse
-> => {
- try {
- const response = await axios.get(`/user/preferences`);
-
- return {
- statusCode: 200,
- error: null,
- message: response.data.status,
- payload: response.data,
- };
- } catch (error) {
- return ErrorResponseHandler(error as AxiosError);
- }
-};
-
-export default getPreference;
diff --git a/frontend/src/api/v1/user/preferences/list.ts b/frontend/src/api/v1/user/preferences/list.ts
new file mode 100644
index 00000000000..f2af94c2c8b
--- /dev/null
+++ b/frontend/src/api/v1/user/preferences/list.ts
@@ -0,0 +1,21 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps } from 'types/api/preferences/list';
+import { UserPreference } from 'types/api/preferences/preference';
+
+const list = async (): Promise> => {
+ try {
+ const response = await axios.get(`/user/preferences`);
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default list;
diff --git a/frontend/src/api/v1/user/preferences/name/get.ts b/frontend/src/api/v1/user/preferences/name/get.ts
new file mode 100644
index 00000000000..941c96f849f
--- /dev/null
+++ b/frontend/src/api/v1/user/preferences/name/get.ts
@@ -0,0 +1,25 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { PayloadProps, Props } from 'types/api/preferences/get';
+import { UserPreference } from 'types/api/preferences/preference';
+
+const get = async (
+ props: Props,
+): Promise> => {
+ try {
+ const response = await axios.get(
+ `/user/preferences/${props.name}`,
+ );
+
+ return {
+ httpStatusCode: response.status,
+ data: response.data.data,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default get;
diff --git a/frontend/src/api/v1/user/preferences/name/update.ts b/frontend/src/api/v1/user/preferences/name/update.ts
new file mode 100644
index 00000000000..1f9605f8772
--- /dev/null
+++ b/frontend/src/api/v1/user/preferences/name/update.ts
@@ -0,0 +1,22 @@
+import axios from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import { Props } from 'types/api/preferences/update';
+
+const update = async (props: Props): Promise> => {
+ try {
+ const response = await axios.put(`/user/preferences/${props.name}`, {
+ value: props.value,
+ });
+
+ return {
+ httpStatusCode: response.status,
+ data: null,
+ };
+ } catch (error) {
+ ErrorResponseHandlerV2(error as AxiosError);
+ }
+};
+
+export default update;
diff --git a/frontend/src/api/v3/licenses/active/get.ts b/frontend/src/api/v3/licenses/active/get.ts
new file mode 100644
index 00000000000..7bf73e95cad
--- /dev/null
+++ b/frontend/src/api/v3/licenses/active/get.ts
@@ -0,0 +1,25 @@
+import { ApiV3Instance as axios } from 'api';
+import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
+import { AxiosError } from 'axios';
+import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
+import {
+ LicenseEventQueueResModel,
+ PayloadProps,
+} from 'types/api/licensesV3/getActive';
+
+const getActive = async (): Promise<
+ SuccessResponseV2