Compare commits

...

47 Commits

Author SHA1 Message Date
primus-bot[bot]
131759ec96 chore(release): bump to v0.80.0 (#7703)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-04-23 12:20:51 +05:30
Shivanshu Raj Shrivastava
365a3e250f chore: fix error rate (#7701)
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-04-22 20:26:37 +00:00
Nityananda Gohain
f3a1f3cc20 fix: handle rate operators for table panel (#7695)
* fix: handle rate operators for table panel
2025-04-22 19:09:08 +00:00
Aditya Singh
ae509b4ae9 Resource attr filter: style fix and quick filter changes (#7691)
* chore: resource attr filter init

* chore: resource attr filter api integration

* chore: operator config updated

* chore: fliter show hide logic and styles

* chore: add support for custom operator list to qb

* chore: minor refactor

* chore: minor code refactor

* test: quick filters test suite added

* test: quick filters test suite added

* test: all errors test suite added

* chore: style fix

* test: all errors mock fix

* chore: test case fix and mixpanel update

* chore: color update

* chore: minor refactor

* chore: style fix

* chore: set default query in exceptions tab

* chore: style fix

* chore: minor refactor

* chore: minor refactor

* chore: minor refactor

* chore: test update

* chore: fix filter header with no query name

* fix: scroll fix

* chore: add data source traces to quick filters

* chore: replace div with fragment

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
2025-04-22 15:45:49 +00:00
Shaheer Kochai
43e2be0333 feat: use search v2 component for traces (#7537)
* Revert "fix: display same key with multiple data types in filter suggestions by enhancing the deduping logic (#7255)"

This reverts commit 1e85981a17.

* fix: use query search v2 for traces data source to handle multiple data types for the same key

* fix(QueryBuilderSearchV2): add user typed option if it doesn't exist in the payload

* fix(QueryBuilderSearchV2): increase the height of search dropdown for non-logs data sources

* fix: display span scope selector for trace data source

* chore: remove the span scope selector from qb search v1 and move the component to search v2

* fix: write test to ensure that we display span scope selector for traces data source

* fix: limit converting  ->   only to log data source

* fix: don't display empty suggestion if only spaces are typed

* chore: tests for span scope selector

* chore: qb search flow (key, operator, value) test cases

* refactor: fix the Maximum update depth reached issue while running tests

* chore: overall improvements to span scope selector tests
2025-04-22 15:24:03 +00:00
Yunus M
20a40b33ce chore: update copy webpack plugin (#7687)
* chore: update copy webpack plugin
2025-04-22 20:36:57 +05:30
sawhil
a9b07c4b47 feat: added test case for checking copying functionality 2025-04-22 15:18:38 +05:30
sawhil
2a5c7cc0ab feat: added test cases for copy span link functionality 2025-04-22 15:18:38 +05:30
sawhil
afb18b8142 fix: pr comments - used useSafeNavigate hook 2025-04-22 15:18:38 +05:30
sawhil
9a580915e6 fix: removed not required prop 2025-04-22 15:18:38 +05:30
sawhil
0944af3d31 feat: added copy span link support alongside span click expand in waterfall graph 2025-04-22 15:18:38 +05:30
SagarRajput-7
9338efcefc fix: boolean values are not shown in the list panel's column (#7668)
* fix: boolean values are not shown in the list panel's column

* fix: moved logic to component level

* fix: added type

* fix: added test cases

* fix: added test cases
2025-04-22 12:03:28 +05:30
sawhil
6b9e0ce799 fix: minor comment update 2025-04-21 12:45:04 +05:30
sawhil
d4c3c24849 feat: added test cases 2025-04-21 12:45:04 +05:30
sawhil
30d935a768 fix: used existing constant 2025-04-21 12:45:04 +05:30
sawhil
073d42c416 fix: removed timestamp and id from being passed to query from log details drawer 2025-04-21 12:45:04 +05:30
Aditya Singh
f11b9644cf Introduce new Resource Attribute FIlter in exceptions tab (#7589)
* chore: resource attr filter init

* chore: resource attr filter api integration

* chore: operator config updated

* chore: fliter show hide logic and styles

* chore: add support for custom operator list to qb

* chore: minor refactor

* chore: minor code refactor

* test: quick filters test suite added

* test: quick filters test suite added

* test: all errors test suite added

* chore: style fix

* test: all errors mock fix

* chore: test case fix and mixpanel update

* chore: color update

* chore: minor refactor

* chore: style fix

* chore: set default query in exceptions tab

* chore: style fix

* chore: minor refactor

* chore: minor refactor

* chore: minor refactor

* chore: test update

* chore: fix filter header with no query name

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
2025-04-21 05:41:05 +00:00
Yunus M
87922e9577 feat: show notification for alert rule ID logic change (#7673)
* feat: show notification for alert rule ID logic change

* Update frontend/src/container/Home/Home.tsx

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>

---------

Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-04-19 14:25:07 +05:30
Prashant Shahi
8412727414 chore: deprecate stagingapp/testingapp (#7649)
### Summary

- deprecate stagingapp/testingapp workflows
- remove `docker-compose.testing.yaml`

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-04-18 15:31:52 +00:00
Srikanth Chekuri
f0a95503d9 chore: add API endpoints for fields keys and values (#7560) 2025-04-18 13:33:17 +00:00
Vikrant Gupta
16e0fa2eef feat(ruler): update the ruler and planned maintenance tables (#7535)
* feat(ruler): base setup for rules and planned maintenance tables

* feat(ruler): more changes for making ruler org aware

* feat(ruler): fix lint

* feat(ruler): update the edit planned maintenance function

* feat(ruler): local testing edits for planned maintenance

* feat(ruler): abstract store and types from rules pkg

* feat(ruler): abstract store and types from rules pkg

* feat(ruler): abstract out store and add migration

* feat(ruler): frontend changes and review comments

* feat(ruler): add back compareAndSelectConfig

* feat(ruler): changes for alertmanager matchers

* feat(ruler): addressed review comments

* feat(ruler): remove the cascade operations from rules table

* feat(ruler): update the template for alertmanager

* feat(ruler): implement the rule history changes

* feat(ruler): implement the rule history changes

* feat(ruler): implement the rule history changes

---------

Co-authored-by: Vibhu Pandey <vibhupandey28@gmail.com>
2025-04-18 00:04:25 +05:30
Yunus M
2fa944d254 feat: update callback url for billing (#7667) 2025-04-17 20:21:14 +05:30
Srikanth Chekuri
b0d19035a4 chore: add where clause visitor implementation for query expression (#7564) 2025-04-17 15:54:36 +05:30
Nityananda Gohain
054dea366e fix: proper formatting of floating point (#7653)
* fix: proper formatting of floating point

* fix: old trace

* fix: add tests

* fix: use strconv.FormatFloat instead
2025-04-17 06:02:41 +00:00
primus-bot[bot]
aaf0b597dc chore(release): bump to v0.79.1 (#7655)
#### Summary
 - Release SigNoz v0.79.1

 Created by [Primus-Bot](https://github.com/apps/primus-bot)

Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-04-17 00:02:05 +05:30
Prashant Shahi
19372c8194 ci(build): use unique cache key for the internal/public builds (#7654)
### Summary

- unique cache keys for the internal/public builds

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-04-16 23:37:05 +05:30
Vibhu Pandey
eb74adad44 test(integration): set the base for integration tests (#7606)
* test(integration): set the base for integration tests

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test

* ci: add ci pipeline for integration test
2025-04-16 18:54:05 +05:30
Srikanth Chekuri
d5c04e1342 chore: log original query failed to transform (#7641) 2025-04-16 14:40:54 +05:30
primus-bot[bot]
2b9632c8fd chore(release): bump to v0.79.0 (#7643)
#### Summary
 - Release SigNoz v0.79.0
 - Bump SigNoz OTel Collector to v0.111.39

 Created by [Primus-Bot](https://github.com/apps/primus-bot)

Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-04-16 13:36:31 +05:30
Prashant Shahi
24920ae903 chore(prereleaser): update cron schedule - 6:30AM UTC (#7640)
### Summary

- update preleaser cron schedule to 6:30AM UTC

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-04-16 07:20:47 +00:00
Prashant Shahi
6f096632a2 chore(build-staging): only include telemetry tunnel FE envs (#7637)
### Summary

- only include telemetry tunnel FE environment variables for the staging build

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-04-16 12:38:30 +05:30
Piyush Singariya
a42eacec4b chore: enhancing JSON Parser handling (#7591)
* feat: enhancing JSON Parser handling

* fix: updating collector version

* chore: updating go.mod reference for Collector

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2025-04-16 11:24:59 +05:30
Nityananda Gohain
e723399f7f fix: add check for empty services (#7611) 2025-04-15 16:39:33 +00:00
Nityananda Gohain
48936bed9b chore: multitenancy in integrations (#7507)
* chore: multitenancy in integrations

* chore: multitenancy in cloud integration accounts

* chore: changes to cloudintegrationservice

* chore: rename migration

* chore: update scan function

* chore: update scan function

* chore: fix migration

* chore: fix struct

* chore: remove unwanted code

* chore: update scan function

* chore: migrate user and pat for integrations

* fix: changes to the user for integrations

* fix: address comments

* fix: copy created_at

* fix: update non revoked token

* chore: don't allow deleting pat and user for integrations

* fix: address comments

* chore: address comments

* chore: add checks for fk in dialect

* fix: service migration

* fix: don't update user if user is already migrated

* fix: update correct service config

* fix: remove unwanted code

* fix: remove migration for multiple same services which is not required

* fix: fix migration and disable disaboard if metrics disabled

* fix: don't use ee types

---------

Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
2025-04-15 15:35:36 +00:00
Srikanth Chekuri
ee70474cc7 fix: missing receivers in json payload for legacy postableAlert (#7603) 2025-04-14 13:20:39 +00:00
Srikanth Chekuri
c3fa7144ee chore: add tag type filter support in attribute keys (#7522) 2025-04-14 18:43:15 +05:30
Nityananda Gohain
5dd02a5b8e fix: remove unnecssary code for email domain check error (#7566)
* fix: proper check for emailComponents

* fix: correct error handling
2025-04-14 11:15:21 +05:30
Srikanth Chekuri
c0f01e4cb9 chore: add metadatastore implementation for logs and traces (#7559)
* chore: add metadatastore implementation for logs and traces

* chore: use telemetrystore mock
2025-04-11 19:41:02 +05:30
Srikanth Chekuri
fed84cb50a chore: add condition builder attributes metadata (#7558) 2025-04-11 16:20:27 +05:30
Srikanth Chekuri
80545c4d07 chore: add materialized field extractor from table schema (#7557) 2025-04-11 15:53:55 +05:30
Srikanth Chekuri
0b1faec092 chore: add condition builder for span index v3 (#7556) 2025-04-11 15:13:04 +05:30
Srikanth Chekuri
ba6f31b1c3 chore: add virtual fields table (#7586) 2025-04-11 07:36:31 +05:30
Srikanth Chekuri
eed92978a4 chore: add non-json condition builder for logs v2 (#7555) 2025-04-10 18:23:01 +00:00
Prashant Shahi
41cbd316b5 Feat/staging (#7585)
### Summary

- Non-production build workflow using Primus
- Staging CD: new staging app and dev staging deployments
- cleanup used docker resources in stagingapp/testingapp machines

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-04-10 17:46:13 +05:30
Vishal Sharma
8d7d33393d feat: include tenant_url in event attributes for logging (#7582) 2025-04-10 15:17:14 +05:30
sawhil
8d143b44b1 feat: removed ff for tp-api-monitoring from fe - 1 2025-04-09 15:44:42 +05:30
sawhil
423aebd6eb feat: removed ff for tp-api-monitoring from fe 2025-04-09 15:44:42 +05:30
220 changed files with 13463 additions and 2708 deletions

View File

@@ -1,6 +1,7 @@
.git .git
.github .github
.vscode .vscode
.devenv
README.md README.md
deploy deploy
sample-apps sample-apps

View File

@@ -2,10 +2,9 @@ name: build-community
on: on:
push: push:
branches:
- main
tags: tags:
- v* - 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+'
defaults: defaults:
run: run:
@@ -19,7 +18,6 @@ jobs:
prepare: prepare:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
docker_providers: ${{ steps.set-docker-providers.outputs.providers }}
version: ${{ steps.build-info.outputs.version }} version: ${{ steps.build-info.outputs.version }}
hash: ${{ steps.build-info.outputs.hash }} hash: ${{ steps.build-info.outputs.hash }}
time: ${{ steps.build-info.outputs.time }} time: ${{ steps.build-info.outputs.time }}
@@ -38,7 +36,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: signoz/primus repository: signoz/primus
ref: ${{ inputs.PRIMUS_REF }} ref: main
path: .primus path: .primus
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: build-info - name: build-info
@@ -47,14 +45,6 @@ jobs:
echo "hash=$($MAKE info-commit-short)" >> $GITHUB_OUTPUT echo "hash=$($MAKE info-commit-short)" >> $GITHUB_OUTPUT
echo "time=$($MAKE info-timestamp)" >> $GITHUB_OUTPUT echo "time=$($MAKE info-timestamp)" >> $GITHUB_OUTPUT
echo "branch=$($MAKE info-branch)" >> $GITHUB_OUTPUT echo "branch=$($MAKE info-branch)" >> $GITHUB_OUTPUT
- name: set-docker-providers
id: set-docker-providers
run: |
if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ || ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
echo "providers=dockerhub gcp" >> $GITHUB_OUTPUT
else
echo "providers=gcp" >> $GITHUB_OUTPUT
fi
js-build: js-build:
uses: signoz/primus.workflows/.github/workflows/js-build.yaml@main uses: signoz/primus.workflows/.github/workflows/js-build.yaml@main
needs: prepare needs: prepare
@@ -88,4 +78,4 @@ jobs:
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}' DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch
DOCKER_MANIFEST: true DOCKER_MANIFEST: true
DOCKER_PROVIDERS: ${{ needs.prepare.outputs.docker_providers }} DOCKER_PROVIDERS: dockerhub

View File

@@ -2,8 +2,6 @@ name: build-enterprise
on: on:
push: push:
branches:
- main
tags: tags:
- v* - v*
@@ -38,7 +36,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: signoz/primus repository: signoz/primus
ref: ${{ inputs.PRIMUS_REF }} ref: main
path: .primus path: .primus
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: build-info - name: build-info
@@ -86,7 +84,7 @@ jobs:
JS_INPUT_ARTIFACT_CACHE_KEY: enterprise-dotenv-${{ github.sha }} JS_INPUT_ARTIFACT_CACHE_KEY: enterprise-dotenv-${{ github.sha }}
JS_INPUT_ARTIFACT_PATH: frontend/.env JS_INPUT_ARTIFACT_PATH: frontend/.env
JS_OUTPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }} JS_OUTPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
JS_OUTPUT_ARTIFACT_PATH: frontend/build JS_OUTPUT_ARTIFACT_PATH: frontend/build
DOCKER_BUILD: false DOCKER_BUILD: false
DOCKER_MANIFEST: false DOCKER_MANIFEST: false
go-build: go-build:

122
.github/workflows/build-staging.yaml vendored Normal file
View File

@@ -0,0 +1,122 @@
name: build-staging
on:
push:
branches:
- main
pull_request:
types: [labeled]
defaults:
run:
shell: bash
env:
PRIMUS_HOME: .primus
MAKE: make --no-print-directory --makefile=.primus/src/make/main.mk
jobs:
prepare:
runs-on: ubuntu-latest
if: ${{ contains(github.event.label.name, 'staging:') || github.event.ref == 'refs/heads/main' }}
outputs:
version: ${{ steps.build-info.outputs.version }}
hash: ${{ steps.build-info.outputs.hash }}
time: ${{ steps.build-info.outputs.time }}
branch: ${{ steps.build-info.outputs.branch }}
deployment: ${{ steps.build-info.outputs.deployment }}
steps:
- name: self-checkout
uses: actions/checkout@v4
- id: token
name: github-token-gen
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PRIMUS_APP_ID }}
private-key: ${{ secrets.PRIMUS_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: primus-checkout
uses: actions/checkout@v4
with:
repository: signoz/primus
ref: main
path: .primus
token: ${{ steps.token.outputs.token }}
- name: build-info
id: build-info
run: |
echo "version=$($MAKE info-version)" >> $GITHUB_OUTPUT
echo "hash=$($MAKE info-commit-short)" >> $GITHUB_OUTPUT
echo "time=$($MAKE info-timestamp)" >> $GITHUB_OUTPUT
echo "branch=$($MAKE info-branch)" >> $GITHUB_OUTPUT
staging_label="${{ github.event.label.name }}"
if [[ "${staging_label}" == "staging:"* ]]; then
deployment=${staging_label#"staging:"}
elif [[ "${{ github.event.ref }}" == "refs/heads/main" ]]; then
deployment="staging"
else
echo "error: not able to determine deployment - please verify the PR label or the branch"
exit 1
fi
echo "deployment=${deployment}" >> $GITHUB_OUTPUT
- name: create-dotenv
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'TUNNEL_URL=https://telemetry.staging.signoz.cloud/tunnel' >> frontend/.env
echo 'TUNNEL_DOMAIN=https://telemetry.staging.signoz.cloud' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:
path: frontend/.env
key: staging-dotenv-${{ github.sha }}
js-build:
uses: signoz/primus.workflows/.github/workflows/js-build.yaml@main
needs: prepare
secrets: inherit
with:
PRIMUS_REF: main
JS_SRC: frontend
JS_INPUT_ARTIFACT_CACHE_KEY: staging-dotenv-${{ github.sha }}
JS_INPUT_ARTIFACT_PATH: frontend/.env
JS_OUTPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
JS_OUTPUT_ARTIFACT_PATH: frontend/build
DOCKER_BUILD: false
DOCKER_MANIFEST: false
go-build:
uses: signoz/primus.workflows/.github/workflows/go-build.yaml@main
needs: [prepare, js-build]
secrets: inherit
with:
PRIMUS_REF: main
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./ee/query-service
GO_BUILD_FLAGS: >-
-tags timetzdata
-ldflags='-linkmode external -extldflags \"-static\" -s -w
-X github.com/SigNoz/signoz/pkg/version.version=${{ needs.prepare.outputs.version }}
-X github.com/SigNoz/signoz/pkg/version.variant=enterprise
-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/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'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
DOCKER_MANIFEST: true
DOCKER_PROVIDERS: gcp
staging:
if: ${{ contains(github.event.label.name, 'staging:') || github.event.ref == 'refs/heads/main' }}
uses: signoz/primus.workflows/.github/workflows/github-trigger.yaml@main
secrets: inherit
needs: [prepare, go-build]
with:
PRIMUS_REF: main
GITHUB_ENVIRONMENT: staging
GITHUB_SILENT: true
GITHUB_REPOSITORY_NAME: charts-saas-v3-staging
GITHUB_EVENT_NAME: releaser
GITHUB_EVENT_PAYLOAD: "{\"deployment\": \"${{ needs.prepare.outputs.deployment }}\", \"signoz_version\": \"${{ needs.prepare.outputs.version }}\"}"

55
.github/workflows/integrationci.yaml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: integrationci
on:
pull_request:
types:
- labeled
pull_request_target:
types:
- labeled
jobs:
test:
strategy:
fail-fast: false
matrix:
src:
- bootstrap
sqlstore-provider:
- postgres
- sqlite
clickhouse-version:
- 24.1.2-alpine
- 24.12-alpine
schema-migrator-version:
- v0.111.38
postgres-version:
- 15
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')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && contains(github.event.pull_request.labels.*.name, 'safe-to-integrate')
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: python
uses: actions/setup-python@v5
with:
python-version: 3.13
- name: poetry
run: |
python -m pip install poetry==2.1.2
python -m poetry config virtualenvs.in-project true
cd tests/integration && poetry install --no-root
- name: run
run: |
cd tests/integration && \
poetry run pytest -ra \
--basetemp=./tmp/ \
-vv \
--capture=no \
src/${{matrix.src}} \
--sqlstore-provider ${{matrix.sqlstore-provider}} \
--postgres-version ${{matrix.postgres-version}} \
--clickhouse-version ${{matrix.clickhouse-version}} \
--schema-migrator-version ${{matrix.schema-migrator-version}}

View File

@@ -1,9 +1,9 @@
name: prereleaser name: prereleaser
on: on:
# schedule every wednesday 9:30 AM UTC (3pm IST) # schedule every wednesday 6:30 AM UTC (12:00 PM IST)
schedule: schedule:
- cron: '30 9 * * 3' - cron: '30 6 * * 3'
# allow manual triggering of the workflow by a maintainer # allow manual triggering of the workflow by a maintainer
workflow_dispatch: workflow_dispatch:

View File

@@ -1,56 +0,0 @@
name: staging-deployment
# Trigger deployment only on push to main branch
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy latest main branch to staging
runs-on: ubuntu-latest
environment: staging
permissions:
contents: 'read'
id-token: 'write'
steps:
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: 'sdk'
uses: 'google-github-actions/setup-gcloud@v2'
- name: 'ssh'
shell: bash
env:
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
GITHUB_SHA: ${{ github.sha }}
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
echo "GITHUB_SHA: ${GITHUB_SHA}"
export VERSION="${GITHUB_SHA:0:7}" # needed for child process to access it
export OTELCOL_TAG="main"
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
export KAFKA_SPAN_EVAL="true"
docker system prune --force
docker pull signoz/signoz-otel-collector:main
docker pull signoz/signoz-schema-migrator:main
cd ~/signoz
git status
git add .
git stash push -m "stashed on $(date --iso-8601=seconds)"
git fetch origin
git checkout ${GITHUB_BRANCH}
git pull
make docker-build-enterprise-amd64
export VERSION="${GITHUB_SHA:0:7}-amd64"
docker-compose -f deploy/docker/docker-compose.testing.yaml up --build -d
EOF
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@@ -1,56 +0,0 @@
name: testing-deployment
# Trigger deployment only on testing-deploy label on pull request
on:
pull_request:
types: [labeled]
jobs:
deploy:
name: Deploy PR branch to testing
runs-on: ubuntu-latest
environment: testing
if: ${{ github.event.label.name == 'testing-deploy' }}
permissions:
contents: 'read'
id-token: 'write'
steps:
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: 'sdk'
uses: 'google-github-actions/setup-gcloud@v2'
- name: 'ssh'
shell: bash
env:
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
GITHUB_SHA: ${{ github.sha }}
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
echo "GITHUB_SHA: ${GITHUB_SHA}"
export VERSION="${GITHUB_SHA:0:7}" # needed for child process to access it
export DEV_BUILD="1"
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
docker system prune --force
cd ~/signoz
git status
git add .
git stash push -m "stashed on $(date --iso-8601=seconds)"
git fetch origin
git checkout main
git pull
# This is added to include the scenerio when new commit in PR is force-pushed
git branch -D ${GITHUB_BRANCH}
git checkout --track origin/${GITHUB_BRANCH}
make docker-build-enterprise-amd64
export VERSION="${GITHUB_SHA:0:7}-amd64"
docker-compose -f deploy/docker/docker-compose.testing.yaml up --build -d
EOF
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

147
.gitignore vendored
View File

@@ -80,6 +80,153 @@ deploy/common/clickhouse/user_scripts/
queries.active queries.active
# tmp
**/tmp/**
# .devenv tmp files # .devenv tmp files
.devenv/**/tmp/** .devenv/**/tmp/**
.qodo
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python

View File

@@ -10,7 +10,7 @@ COMMIT_SHORT_SHA ?= $(shell git rev-parse --short HEAD)
BRANCH_NAME ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) BRANCH_NAME ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
VERSION ?= $(BRANCH_NAME)-$(COMMIT_SHORT_SHA) VERSION ?= $(BRANCH_NAME)-$(COMMIT_SHORT_SHA)
TIMESTAMP ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") TIMESTAMP ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
ARCHS = amd64 arm64 ARCHS ?= amd64 arm64
TARGET_DIR ?= $(shell pwd)/target TARGET_DIR ?= $(shell pwd)/target
ZEUS_URL ?= https://api.signoz.cloud ZEUS_URL ?= https://api.signoz.cloud
@@ -23,6 +23,7 @@ GO_BUILD_ARCHS_COMMUNITY = $(addprefix go-build-community-,$(ARCHS))
GO_BUILD_CONTEXT_COMMUNITY = $(SRC)/pkg/query-service GO_BUILD_CONTEXT_COMMUNITY = $(SRC)/pkg/query-service
GO_BUILD_LDFLAGS_COMMUNITY = $(GO_BUILD_VERSION_LDFLAGS) -X github.com/SigNoz/signoz/pkg/version.variant=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 = $(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)/ee/query-service
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) 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)
@@ -119,6 +120,18 @@ $(GO_BUILD_ARCHS_ENTERPRISE): go-build-enterprise-%: $(TARGET_DIR)
CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \ CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
fi fi
.PHONY: go-build-enterprise-race $(GO_BUILD_ARCHS_ENTERPRISE_RACE)
go-build-enterprise-race: ## Builds the go backend server for enterprise with race
go-build-enterprise-race: $(GO_BUILD_ARCHS_ENTERPRISE_RACE)
$(GO_BUILD_ARCHS_ENTERPRISE_RACE): go-build-enterprise-race-%: $(TARGET_DIR)
@mkdir -p $(TARGET_DIR)/$(OS)-$*
@echo ">> building binary $(TARGET_DIR)/$(OS)-$*/$(NAME)"
@if [ $* = "arm64" ]; then \
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -race -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
else \
CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -race -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
fi
############################################################## ##############################################################
# js commands # js commands
############################################################## ##############################################################
@@ -167,3 +180,20 @@ docker-buildx-enterprise: go-build-enterprise js-build
--platform linux/arm64,linux/amd64 \ --platform linux/arm64,linux/amd64 \
--push \ --push \
--tag $(DOCKER_REGISTRY_ENTERPRISE):$(VERSION) $(SRC) --tag $(DOCKER_REGISTRY_ENTERPRISE):$(VERSION) $(SRC)
##############################################################
# python commands
##############################################################
.PHONY: py-fmt
py-fmt: ## Run black for integration tests
@cd tests/integration && poetry run black .
.PHONY: py-lint
py-lint: ## Run lint for integration tests
@cd tests/integration && poetry run isort .
@cd tests/integration && poetry run autoflake .
@cd tests/integration && poetry run pylint .
.PHONY: py-test
py-test: ## Runs integration tests
@cd tests/integration && poetry run pytest --basetemp=./tmp/ -vv --capture=no src/

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz: signoz:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz:v0.78.1 image: signoz/signoz:v0.80.0
command: command:
- --config=/root/config/prometheus.yml - --config=/root/config/prometheus.yml
- --use-logs-new-schema=true - --use-logs-new-schema=true
@@ -208,7 +208,7 @@ services:
retries: 3 retries: 3
otel-collector: otel-collector:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.111.38 image: signoz/signoz-otel-collector:v0.111.39
command: command:
- --config=/etc/otel-collector-config.yaml - --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml - --manager-config=/etc/manager-config.yaml
@@ -232,7 +232,7 @@ services:
- signoz - signoz
schema-migrator: schema-migrator:
!!merge <<: *common !!merge <<: *common
image: signoz/signoz-schema-migrator:v0.111.38 image: signoz/signoz-schema-migrator:v0.111.39
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz: signoz:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz:v0.78.1 image: signoz/signoz:v0.80.0
command: command:
- --config=/root/config/prometheus.yml - --config=/root/config/prometheus.yml
- --use-logs-new-schema=true - --use-logs-new-schema=true
@@ -143,7 +143,7 @@ services:
retries: 3 retries: 3
otel-collector: otel-collector:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.111.38 image: signoz/signoz-otel-collector:v0.111.39
command: command:
- --config=/etc/otel-collector-config.yaml - --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml - --manager-config=/etc/manager-config.yaml
@@ -167,7 +167,7 @@ services:
- signoz - signoz
schema-migrator: schema-migrator:
!!merge <<: *common !!merge <<: *common
image: signoz/signoz-schema-migrator:v0.111.38 image: signoz/signoz-schema-migrator:v0.111.39
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz: signoz:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.78.1} image: signoz/signoz:${VERSION:-v0.80.0}
container_name: signoz container_name: signoz
command: command:
- --config=/root/config/prometheus.yml - --config=/root/config/prometheus.yml
@@ -212,7 +212,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing? # TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector: otel-collector:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.38} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.39}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
- --config=/etc/otel-collector-config.yaml - --config=/etc/otel-collector-config.yaml
@@ -238,7 +238,7 @@ services:
condition: service_healthy condition: service_healthy
schema-migrator-sync: schema-migrator-sync:
!!merge <<: *common !!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
container_name: schema-migrator-sync container_name: schema-migrator-sync
command: command:
- sync - sync
@@ -249,7 +249,7 @@ services:
condition: service_healthy condition: service_healthy
schema-migrator-async: schema-migrator-async:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
container_name: schema-migrator-async container_name: schema-migrator-async
command: command:
- async - async

View File

@@ -1,199 +0,0 @@
version: "3"
x-common: &common
networks:
- signoz-net
restart: unless-stopped
logging:
options:
max-size: 50m
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
tty: true
labels:
signoz.io/scrape: "true"
signoz.io/port: "9363"
signoz.io/path: "/metrics"
depends_on:
init-clickhouse:
condition: service_completed_successfully
zookeeper-1:
condition: service_healthy
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- 0.0.0.0:8123/ping
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
signoz.io/port: "9141"
signoz.io/path: "/metrics"
healthcheck:
test:
- CMD-SHELL
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
interval: 30s
timeout: 5s
retries: 3
x-db-depend: &db-depend
!!merge <<: *common
depends_on:
clickhouse:
condition: service_healthy
schema-migrator-sync:
condition: service_completed_successfully
services:
init-clickhouse:
!!merge <<: *common
image: clickhouse/clickhouse-server:24.1.2-alpine
container_name: signoz-init-clickhouse
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
restart: on-failure
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-1
ports:
- "2181:2181"
- "2888:2888"
- "3888:3888"
volumes:
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse
ports:
- "9000:9000"
- "8123:8123"
- "9181:9181"
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:${VERSION:-v0.78.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
- --gateway-url=https://api.staging.signoz.cloud
- --use-logs-new-schema=true
- --use-trace-new-schema=true
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/
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
- DASHBOARDS_PATH=/root/config/dashboards
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:8080/api/v1/health
interval: 30s
timeout: 5s
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.38}
container_name: signoz-otel-collector
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
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
depends_on:
signoz:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38}
container_name: schema-migrator-sync
command:
- sync
- --dsn=tcp://clickhouse:9000
- --up=
depends_on:
clickhouse:
condition: service_healthy
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38}
container_name: schema-migrator-async
command:
- async
- --dsn=tcp://clickhouse:9000
- --up=
restart: on-failure
networks:
signoz-net:
name: signoz-net
volumes:
clickhouse:
name: signoz-clickhouse
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz: signoz:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.78.1} image: signoz/signoz:${VERSION:-v0.80.0}
container_name: signoz container_name: signoz
command: command:
- --config=/root/config/prometheus.yml - --config=/root/config/prometheus.yml
@@ -144,7 +144,7 @@ services:
retries: 3 retries: 3
otel-collector: otel-collector:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.38} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.39}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
- --config=/etc/otel-collector-config.yaml - --config=/etc/otel-collector-config.yaml
@@ -166,7 +166,7 @@ services:
condition: service_healthy condition: service_healthy
schema-migrator-sync: schema-migrator-sync:
!!merge <<: *common !!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
container_name: schema-migrator-sync container_name: schema-migrator-sync
command: command:
- sync - sync
@@ -178,7 +178,7 @@ services:
restart: on-failure restart: on-failure
schema-migrator-async: schema-migrator-async:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
container_name: schema-migrator-async container_name: schema-migrator-async
command: command:
- async - async

View File

@@ -0,0 +1,36 @@
FROM golang:1.22-bullseye
ARG OS="linux"
ARG TARGETARCH
ARG ZEUSURL
# This path is important for stacktraces
WORKDIR $GOPATH/src/github.com/signoz/signoz
WORKDIR /root
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
g++ \
gcc \
libc6-dev \
make \
pkg-config \
; \
rm -rf /var/lib/apt/lists/*
COPY go.mod go.sum ./
RUN go mod download
COPY ./ee/ ./ee/
COPY ./pkg/ ./pkg/
COPY ./templates/email /root/templates
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
RUN chmod 755 /root /root/signoz
ENTRYPOINT ["/root/signoz"]

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz/ee/query-service/license" "github.com/SigNoz/signoz/ee/query-service/license"
"github.com/SigNoz/signoz/ee/query-service/usage" "github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/modules/preference" "github.com/SigNoz/signoz/pkg/modules/preference"
preferencecore "github.com/SigNoz/signoz/pkg/modules/preference/core" preferencecore "github.com/SigNoz/signoz/pkg/modules/preference/core"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app" baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
@@ -74,6 +75,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
UseLogsNewSchema: opts.UseLogsNewSchema, UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema, UseTraceNewSchema: opts.UseTraceNewSchema,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager), AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
FieldsAPI: fields.NewAPI(signoz.TelemetryStore),
Signoz: signoz, Signoz: signoz,
Preference: preference, Preference: preference,
}) })

View File

@@ -153,9 +153,11 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
func (ah *APIHandler) getOrCreateCloudIntegrationUser( func (ah *APIHandler) getOrCreateCloudIntegrationUser(
ctx context.Context, orgId string, cloudProvider string, ctx context.Context, orgId string, cloudProvider string,
) (*types.User, *basemodel.ApiError) { ) (*types.User, *basemodel.ApiError) {
cloudIntegrationUserId := fmt.Sprintf("%s-integration", cloudProvider) cloudIntegrationUser := fmt.Sprintf("%s-integration", cloudProvider)
email := fmt.Sprintf("%s@signoz.io", cloudIntegrationUser)
integrationUserResult, apiErr := ah.AppDao().GetUser(ctx, cloudIntegrationUserId) // TODO(nitya): there should be orgId here
integrationUserResult, apiErr := ah.AppDao().GetUserByEmail(ctx, email)
if apiErr != nil { if apiErr != nil {
return nil, basemodel.WrapApiError(apiErr, "couldn't look for integration user") return nil, basemodel.WrapApiError(apiErr, "couldn't look for integration user")
} }
@@ -170,9 +172,9 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
) )
newUser := &types.User{ newUser := &types.User{
ID: cloudIntegrationUserId, ID: uuid.New().String(),
Name: fmt.Sprintf("%s integration", cloudProvider), Name: cloudIntegrationUser,
Email: fmt.Sprintf("%s@signoz.io", cloudIntegrationUserId), Email: email,
TimeAuditable: types.TimeAuditable{ TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(), CreatedAt: time.Now(),
}, },

View File

@@ -5,16 +5,18 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"slices"
"time" "time"
"github.com/SigNoz/signoz/ee/query-service/model" "github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/ee/types"
eeTypes "github.com/SigNoz/signoz/ee/types" eeTypes "github.com/SigNoz/signoz/ee/types"
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/query-service/auth" "github.com/SigNoz/signoz/pkg/query-service/auth"
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants" baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model" basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"go.uber.org/zap" "go.uber.org/zap"
@@ -58,7 +60,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
ah.Respond(w, &pat) ah.Respond(w, &pat)
} }
func validatePATRequest(req types.GettablePAT) error { func validatePATRequest(req eeTypes.GettablePAT) error {
if req.Role == "" || (req.Role != baseconstants.ViewerGroup && req.Role != baseconstants.EditorGroup && req.Role != baseconstants.AdminGroup) { if req.Role == "" || (req.Role != baseconstants.ViewerGroup && req.Role != baseconstants.EditorGroup && req.Role != baseconstants.AdminGroup) {
return fmt.Errorf("valid role is required") return fmt.Errorf("valid role is required")
} }
@@ -74,12 +76,19 @@ func validatePATRequest(req types.GettablePAT) error {
func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) { func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
ctx := context.Background() ctx := context.Background()
req := types.GettablePAT{} req := eeTypes.GettablePAT{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, model.BadRequest(err), nil) RespondError(w, model.BadRequest(err), nil)
return 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
}
user, err := auth.GetUserFromReqContext(r.Context()) user, err := auth.GetUserFromReqContext(r.Context())
if err != nil { if err != nil {
RespondError(w, &model.ApiError{ RespondError(w, &model.ApiError{
@@ -89,6 +98,25 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
return return
} }
//get the pat
existingPAT, paterr := ah.AppDao().GetPATByID(ctx, user.OrgID, id)
if paterr != nil {
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, paterr.Error()))
return
}
// get the user
createdByUser, usererr := ah.AppDao().GetUser(ctx, existingPAT.UserID)
if usererr != nil {
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, usererr.Error()))
return
}
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "integration user pat cannot be updated"))
return
}
err = validatePATRequest(req) err = validatePATRequest(req)
if err != nil { if err != nil {
RespondError(w, model.BadRequest(err), nil) RespondError(w, model.BadRequest(err), nil)
@@ -96,12 +124,6 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
} }
req.UpdatedByUserID = user.ID req.UpdatedByUserID = user.ID
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
}
req.UpdatedAt = time.Now() req.UpdatedAt = time.Now()
zap.L().Info("Got Update PAT request", zap.Any("pat", req)) zap.L().Info("Got Update PAT request", zap.Any("pat", req))
var apierr basemodel.BaseApiError var apierr basemodel.BaseApiError
@@ -149,6 +171,25 @@ func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
return return
} }
//get the pat
existingPAT, paterr := ah.AppDao().GetPATByID(ctx, user.OrgID, id)
if paterr != nil {
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, paterr.Error()))
return
}
// get the user
createdByUser, usererr := ah.AppDao().GetUser(ctx, existingPAT.UserID)
if usererr != nil {
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, usererr.Error()))
return
}
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "integration user pat cannot be updated"))
return
}
zap.L().Info("Revoke PAT with id", zap.String("id", id.StringValue())) zap.L().Info("Revoke PAT with id", zap.String("id", id.StringValue()))
if apierr := ah.AppDao().RevokePAT(ctx, user.OrgID, id, user.ID); apierr != nil { if apierr := ah.AppDao().RevokePAT(ctx, user.OrgID, id, user.ID); apierr != nil {
RespondError(w, apierr, nil) RespondError(w, apierr, nil)

View File

@@ -367,6 +367,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterLogsRoutes(r, am) apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am) apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterCloudIntegrationsRoutes(r, am) apiHandler.RegisterCloudIntegrationsRoutes(r, am)
apiHandler.RegisterFieldsRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am) apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am) apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am) apiHandler.RegisterQueryRangeV4Routes(r, am)
@@ -428,11 +429,11 @@ func (s *Server) initListeners() error {
} }
// Start listening on http and private http port concurrently // Start listening on http and private http port concurrently
func (s *Server) Start() error { func (s *Server) Start(ctx context.Context) error {
// initiate rule manager first // initiate rule manager first
if !s.serverOptions.DisableRules { if !s.serverOptions.DisableRules {
s.ruleManager.Start() s.ruleManager.Start(ctx)
} else { } else {
zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE") zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE")
} }
@@ -516,7 +517,7 @@ func (s *Server) Stop() error {
s.opampServer.Stop() s.opampServer.Stop()
if s.ruleManager != nil { if s.ruleManager != nil {
s.ruleManager.Stop() s.ruleManager.Stop(context.Background())
} }
// stop usage manager // stop usage manager

View File

@@ -8,7 +8,6 @@ import (
basedao "github.com/SigNoz/signoz/pkg/query-service/dao" basedao "github.com/SigNoz/signoz/pkg/query-service/dao"
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces" baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model" basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
ossTypes "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/valuer"
"github.com/google/uuid" "github.com/google/uuid"
@@ -40,7 +39,6 @@ type ModelDao interface {
UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id valuer.UUID) basemodel.BaseApiError UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id valuer.UUID) basemodel.BaseApiError
GetPAT(ctx context.Context, pat string) (*types.GettablePAT, basemodel.BaseApiError) GetPAT(ctx context.Context, pat string) (*types.GettablePAT, basemodel.BaseApiError)
GetPATByID(ctx context.Context, orgID string, id valuer.UUID) (*types.GettablePAT, basemodel.BaseApiError) GetPATByID(ctx context.Context, orgID string, id valuer.UUID) (*types.GettablePAT, basemodel.BaseApiError)
GetUserByPAT(ctx context.Context, orgID string, token string) (*ossTypes.GettableUser, basemodel.BaseApiError)
ListPATs(ctx context.Context, orgID string) ([]types.GettablePAT, basemodel.BaseApiError) ListPATs(ctx context.Context, orgID string) ([]types.GettablePAT, basemodel.BaseApiError)
RevokePAT(ctx context.Context, orgID string, id valuer.UUID, userID string) basemodel.BaseApiError RevokePAT(ctx context.Context, orgID string, id valuer.UUID, userID string) basemodel.BaseApiError
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"net/url" "net/url"
"strings"
"time" "time"
"github.com/SigNoz/signoz/ee/query-service/constants" "github.com/SigNoz/signoz/ee/query-service/constants"
@@ -44,7 +43,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
} }
user := &types.User{ user := &types.User{
ID: uuid.NewString(), ID: uuid.New().String(),
Name: "", Name: "",
Email: email, Email: email,
Password: hash, Password: hash,
@@ -162,12 +161,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
// find domain from email // find domain from email
orgDomain, apierr := m.GetDomainByEmail(ctx, email) orgDomain, apierr := m.GetDomainByEmail(ctx, email)
if apierr != nil { if apierr != nil {
var emailDomain string zap.L().Error("failed to get org domain from email", zap.String("email", email), zap.Error(apierr.ToError()))
emailComponents := strings.Split(email, "@")
if len(emailComponents) > 0 {
emailDomain = emailComponents[1]
}
zap.L().Error("failed to get org domain from email", zap.String("emailDomain", emailDomain), zap.Error(apierr.ToError()))
return resp, apierr return resp, apierr
} }

View File

@@ -196,27 +196,3 @@ func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id valuer.UUID)
return &patWithUser, nil return &patWithUser, nil
} }
// deprecated
func (m *modelDao) GetUserByPAT(ctx context.Context, orgID string, token string) (*ossTypes.GettableUser, basemodel.BaseApiError) {
users := []ossTypes.GettableUser{}
if err := m.DB().NewSelect().
Model(&users).
Column("u.id", "u.name", "u.email", "u.password", "u.created_at", "u.profile_picture_url", "u.org_id", "u.group_id").
Join("JOIN personal_access_tokens p ON u.id = p.user_id").
Where("p.token = ?", token).
Where("p.expires_at >= strftime('%s', 'now')").
Where("p.org_id = ?", orgID).
Scan(ctx); err != nil {
return nil, model.InternalError(fmt.Errorf("failed to fetch user from PAT, err: %v", err))
}
if len(users) != 1 {
return nil, &model.ApiError{
Typ: model.ErrorInternal,
Err: fmt.Errorf("found zero or multiple users with same PAT token"),
}
}
return &users[0], nil
}

View File

@@ -143,7 +143,7 @@ func main() {
zap.L().Fatal("Failed to create server", zap.Error(err)) zap.L().Fatal("Failed to create server", zap.Error(err))
} }
if err := server.Start(); err != nil { if err := server.Start(context.Background()); err != nil {
zap.L().Fatal("Could not start server", zap.Error(err)) zap.L().Fatal("Could not start server", zap.Error(err))
} }

View File

@@ -15,6 +15,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/cache" "github.com/SigNoz/signoz/pkg/query-service/cache"
"github.com/SigNoz/signoz/pkg/query-service/common" "github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/query-service/model"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2" querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2"
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder" "github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
@@ -52,7 +53,7 @@ type AnomalyRule struct {
func NewAnomalyRule( func NewAnomalyRule(
id string, id string,
p *baserules.PostableRule, p *ruletypes.PostableRule,
reader interfaces.Reader, reader interfaces.Reader,
cache cache.Cache, cache cache.Cache,
opts ...baserules.RuleOption, opts ...baserules.RuleOption,
@@ -60,7 +61,7 @@ func NewAnomalyRule(
zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts)) zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts))
if p.RuleCondition.CompareOp == baserules.ValueIsBelow { if p.RuleCondition.CompareOp == ruletypes.ValueIsBelow {
target := -1 * *p.RuleCondition.Target target := -1 * *p.RuleCondition.Target
p.RuleCondition.Target = &target p.RuleCondition.Target = &target
} }
@@ -117,7 +118,7 @@ func NewAnomalyRule(
return &t, nil return &t, nil
} }
func (r *AnomalyRule) Type() baserules.RuleType { func (r *AnomalyRule) Type() ruletypes.RuleType {
return RuleTypeAnomaly return RuleTypeAnomaly
} }
@@ -157,7 +158,7 @@ func (r *AnomalyRule) GetSelectedQuery() string {
return r.Condition().GetSelectedQueryName() return r.Condition().GetSelectedQueryName()
} }
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (baserules.Vector, error) { func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (ruletypes.Vector, error) {
params, err := r.prepareQueryRange(ts) params, err := r.prepareQueryRange(ts)
if err != nil { if err != nil {
@@ -184,7 +185,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (baser
} }
} }
var resultVector baserules.Vector var resultVector ruletypes.Vector
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores) scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON))) zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON)))
@@ -213,7 +214,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
defer r.mtx.Unlock() defer r.mtx.Unlock()
resultFPs := map[uint64]struct{}{} resultFPs := map[uint64]struct{}{}
var alerts = make(map[uint64]*baserules.Alert, len(res)) var alerts = make(map[uint64]*ruletypes.Alert, len(res))
for _, smpl := range res { for _, smpl := range res {
l := make(map[string]string, len(smpl.Metric)) l := make(map[string]string, len(smpl.Metric))
@@ -225,7 +226,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
threshold := valueFormatter.Format(r.TargetVal(), 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)) 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))
tmplData := baserules.AlertTemplateData(l, value, threshold) tmplData := ruletypes.AlertTemplateData(l, value, threshold)
// Inject some convenience variables that are easier to remember for users // Inject some convenience variables that are easier to remember for users
// who are not used to Go's templating system. // who are not used to Go's templating system.
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}" defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
@@ -233,7 +234,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
// utility function to apply go template on labels and annotations // utility function to apply go template on labels and annotations
expand := func(text string) string { expand := func(text string) string {
tmpl := baserules.NewTemplateExpander( tmpl := ruletypes.NewTemplateExpander(
ctx, ctx,
defs+text, defs+text,
"__alert_"+r.Name(), "__alert_"+r.Name(),
@@ -278,7 +279,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
return nil, err return nil, err
} }
alerts[h] = &baserules.Alert{ alerts[h] = &ruletypes.Alert{
Labels: lbs, Labels: lbs,
QueryResultLables: resultLabels, QueryResultLables: resultLabels,
Annotations: annotations, Annotations: annotations,
@@ -319,7 +320,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
if _, ok := resultFPs[fp]; !ok { if _, ok := resultFPs[fp]; !ok {
// If the alert was previously firing, keep it around for a given // If the alert was previously firing, keep it around for a given
// retention time so it is reported as resolved to the AlertManager. // retention time so it is reported as resolved to the AlertManager.
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > baserules.ResolvedRetention) { if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
delete(r.Active, fp) delete(r.Active, fp)
} }
if a.State != model.StateInactive { if a.State != model.StateInactive {
@@ -375,10 +376,10 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
func (r *AnomalyRule) String() string { func (r *AnomalyRule) String() string {
ar := baserules.PostableRule{ ar := ruletypes.PostableRule{
AlertName: r.Name(), AlertName: r.Name(),
RuleCondition: r.Condition(), RuleCondition: r.Condition(),
EvalWindow: baserules.Duration(r.EvalWindow()), EvalWindow: ruletypes.Duration(r.EvalWindow()),
Labels: r.Labels().Map(), Labels: r.Labels().Map(),
Annotations: r.Annotations().Map(), Annotations: r.Annotations().Map(),
PreferredChannels: r.PreferredChannels(), PreferredChannels: r.PreferredChannels(),

View File

@@ -8,6 +8,7 @@ import (
basemodel "github.com/SigNoz/signoz/pkg/query-service/model" basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules" baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels" "github.com/SigNoz/signoz/pkg/query-service/utils/labels"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -18,7 +19,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
var task baserules.Task var task baserules.Task
ruleId := baserules.RuleIdFromTaskName(opts.TaskName) ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
if opts.Rule.RuleType == baserules.RuleTypeThreshold { if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule // create a threshold rule
tr, err := baserules.NewThresholdRule( tr, err := baserules.NewThresholdRule(
ruleId, ruleId,
@@ -37,9 +38,9 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, tr) rules = append(rules, tr)
// create ch rule task for evalution // create ch rule task for evalution
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB) task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == baserules.RuleTypeProm { } else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
// create promql rule // create promql rule
pr, err := baserules.NewPromRule( pr, err := baserules.NewPromRule(
@@ -58,9 +59,9 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, pr) rules = append(rules, pr)
// create promql rule task for evalution // create promql rule task for evalution
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB) task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == baserules.RuleTypeAnomaly { } else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
// create anomaly rule // create anomaly rule
ar, err := NewAnomalyRule( ar, err := NewAnomalyRule(
ruleId, ruleId,
@@ -77,10 +78,10 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, ar) rules = append(rules, ar)
// create anomaly rule task for evalution // create anomaly rule task for evalution
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB) task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else { } else {
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, baserules.RuleTypeProm, baserules.RuleTypeThreshold) return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
} }
return task, nil return task, nil
@@ -105,12 +106,12 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
} }
// append name to indicate this is test alert // append name to indicate this is test alert
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, baserules.TestAlertPostFix) parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, ruletypes.TestAlertPostFix)
var rule baserules.Rule var rule baserules.Rule
var err error var err error
if parsedRule.RuleType == baserules.RuleTypeThreshold { if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
// add special labels for test alerts // 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.Annotations[labels.AlertSummaryLabel] = fmt.Sprintf("The rule threshold is set to %.4f, and the observed metric value is {{$value}}.", *parsedRule.RuleCondition.Target)
@@ -134,7 +135,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
return 0, basemodel.BadRequest(err) return 0, basemodel.BadRequest(err)
} }
} else if parsedRule.RuleType == baserules.RuleTypeProm { } else if parsedRule.RuleType == ruletypes.RuleTypeProm {
// create promql rule // create promql rule
rule, err = baserules.NewPromRule( rule, err = baserules.NewPromRule(
@@ -152,7 +153,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", rule.Name()), zap.Error(err)) zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", rule.Name()), zap.Error(err))
return 0, basemodel.BadRequest(err) return 0, basemodel.BadRequest(err)
} }
} else if parsedRule.RuleType == baserules.RuleTypeAnomaly { } else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
// create anomaly rule // create anomaly rule
rule, err = NewAnomalyRule( rule, err = NewAnomalyRule(
alertname, alertname,
@@ -190,9 +191,9 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
// newTask returns an appropriate group for // newTask returns an appropriate group for
// rule type // rule type
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task { func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID string) baserules.Task {
if taskType == baserules.TaskTypeCh { if taskType == baserules.TaskTypeCh {
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, ruleDB) return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
} }
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, ruleDB) return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
} }

View File

@@ -17,13 +17,15 @@ var (
) )
var ( var (
Org = "org" Org = "org"
User = "user" User = "user"
CloudIntegration = "cloud_integration"
) )
var ( var (
OrgReference = `("org_id") REFERENCES "organizations" ("id")` OrgReference = `("org_id") REFERENCES "organizations" ("id")`
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE` UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
) )
type dialect struct { type dialect struct {
@@ -211,6 +213,8 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
fkReferences = append(fkReferences, OrgReference) fkReferences = append(fkReferences, OrgReference)
} else if reference == User && !slices.Contains(fkReferences, UserReference) { } else if reference == User && !slices.Contains(fkReferences, UserReference) {
fkReferences = append(fkReferences, UserReference) fkReferences = append(fkReferences, UserReference)
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
fkReferences = append(fkReferences, CloudIntegrationReference)
} }
} }

View File

@@ -198,7 +198,7 @@
"autoprefixer": "10.4.19", "autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0", "babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0", "compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^8.1.0", "copy-webpack-plugin": "^11.0.0",
"critters-webpack-plugin": "^3.0.1", "critters-webpack-plugin": "^3.0.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
@@ -255,6 +255,7 @@
"body-parser": "1.20.3", "body-parser": "1.20.3",
"http-proxy-middleware": "3.0.3", "http-proxy-middleware": "3.0.3",
"cross-spawn": "7.0.5", "cross-spawn": "7.0.5",
"cookie": "^0.7.1" "cookie": "^0.7.1",
"serialize-javascript": "6.0.2"
} }
} }

View File

@@ -1,3 +1,4 @@
import { isEmpty } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/save'; import { PayloadProps, Props } from 'types/api/alerts/save';
@@ -7,7 +8,7 @@ import put from './put';
const save = async ( const save = async (
props: Props, props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => { ): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
if (props.id && props.id > 0) { if (props.id && !isEmpty(props.id)) {
return put({ ...props }); return put({ ...props });
} }

View File

@@ -11,9 +11,12 @@ const logEvent = async (
rateLimited?: boolean, rateLimited?: boolean,
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => { ): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
try { try {
// add tenant_url to attributes
const { hostname } = window.location;
const updatedAttributes = { ...attributes, tenant_url: hostname };
const response = await axios.post('/event', { const response = await axios.post('/event', {
eventName, eventName,
attributes, attributes: updatedAttributes,
eventType: eventType || 'track', eventType: eventType || 'track',
rateLimited: rateLimited || false, // TODO: Update this once we have a proper way to handle rate limiting rateLimited: rateLimited || false, // TODO: Update this once we have a proper way to handle rate limiting
}); });

View File

@@ -45,6 +45,7 @@ export default function ChatSupportGateway(): JSX.Element {
}, },
); );
const { pathname } = useLocation(); const { pathname } = useLocation();
const handleAddCreditCard = (): void => { const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', { logEvent('Add Credit card modal: Clicked', {
source: `intercom icon`, source: `intercom icon`,
@@ -52,7 +53,7 @@ export default function ChatSupportGateway(): JSX.Element {
}); });
updateCreditCard({ updateCreditCard({
url: window.location.href, url: window.location.origin,
}); });
}; };

View File

@@ -153,7 +153,7 @@ function LaunchChatSupport({
}); });
updateCreditCard({ updateCreditCard({
url: window.location.href, url: window.location.origin,
}); });
}; };

View File

@@ -6,6 +6,7 @@ import {
VerticalAlignTopOutlined, VerticalAlignTopOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Tooltip, Typography } from 'antd'; import { Tooltip, Typography } from 'antd';
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isFunction } from 'lodash-es'; import { cloneDeep, isFunction } from 'lodash-es';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -68,10 +69,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
<section className="header"> <section className="header">
<section className="left-actions"> <section className="left-actions">
<FilterOutlined /> <FilterOutlined />
<Typography.Text className="text">Filters for</Typography.Text> <Typography.Text className="text">
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}> {lastQueryName ? 'Filters for' : 'Filters'}
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text> </Typography.Text>
</Tooltip> {lastQueryName && (
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
</Tooltip>
)}
</section> </section>
<section className="right-actions"> <section className="right-actions">
@@ -89,31 +94,33 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
</section> </section>
)} )}
<section className="filters"> <TypicalOverlayScrollbar>
{config.map((filter) => { <section className="filters">
switch (filter.type) { {config.map((filter) => {
case FiltersType.CHECKBOX: switch (filter.type) {
return ( case FiltersType.CHECKBOX:
<Checkbox return (
source={source} <Checkbox
filter={filter} source={source}
onFilterChange={onFilterChange} filter={filter}
/> onFilterChange={onFilterChange}
); />
case FiltersType.SLIDER: );
return <Slider filter={filter} />; case FiltersType.SLIDER:
// eslint-disable-next-line sonarjs/no-duplicated-branches return <Slider filter={filter} />;
default: // eslint-disable-next-line sonarjs/no-duplicated-branches
return ( default:
<Checkbox return (
source={source} <Checkbox
filter={filter} source={source}
onFilterChange={onFilterChange} filter={filter}
/> onFilterChange={onFilterChange}
); />
} );
})} }
</section> })}
</section>
</TypicalOverlayScrollbar>
</div> </div>
); );
} }

View File

@@ -0,0 +1,111 @@
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import QuickFilters from '../QuickFilters';
import { QuickFiltersSource } from '../types';
import { QuickFiltersConfig } from './constants';
// Mock the useQueryBuilder hook
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
// Mock the useGetAggregateValues hook
jest.mock('hooks/queryBuilder/useGetAggregateValues', () => ({
useGetAggregateValues: jest.fn(),
}));
const handleFilterVisibilityChange = jest.fn();
const redirectWithQueryBuilderData = jest.fn();
function TestQuickFilters(): JSX.Element {
return (
<MockQueryClientProvider>
<QuickFilters
source={QuickFiltersSource.EXCEPTIONS}
config={QuickFiltersConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
/>
</MockQueryClientProvider>
);
}
describe('Quick Filters', () => {
beforeEach(() => {
// Provide a mock implementation for useQueryBuilder
(useQueryBuilder as jest.Mock).mockReturnValue({
currentQuery: {
builder: {
queryData: [
{
queryName: 'Test Query',
filters: { items: [{ key: 'test', value: 'value' }] },
},
],
},
},
lastUsedQuery: 0,
redirectWithQueryBuilderData,
});
// Provide a mock implementation for useGetAggregateValues
(useGetAggregateValues as jest.Mock).mockReturnValue({
data: {
statusCode: 200,
error: null,
message: 'success',
payload: {
stringAttributeValues: [
'mq-kafka',
'otel-demo',
'otlp-python',
'sample-flask',
],
numberAttributeValues: null,
boolAttributeValues: null,
},
}, // Mocked API response
isLoading: false,
});
});
it('renders correctly with default props', () => {
const { container } = render(<TestQuickFilters />);
expect(container).toMatchSnapshot();
});
it('displays the correct query name in the header', () => {
render(<TestQuickFilters />);
expect(screen.getByText('Filters for')).toBeInTheDocument();
expect(screen.getByText('Test Query')).toBeInTheDocument();
});
it('should add filter data to query when checkbox is clicked', () => {
render(<TestQuickFilters />);
const checkbox = screen.getByText('mq-kafka');
fireEvent.click(checkbox);
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
expect.objectContaining({
builder: {
queryData: expect.arrayContaining([
expect.objectContaining({
filters: expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({
key: 'deployment.environment',
}),
value: 'mq-kafka',
}),
]),
}),
}),
]),
},
}),
); // sets composite query param
});
});

View File

@@ -0,0 +1,382 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Quick Filters renders correctly with default props 1`] = `
<div>
<div
class="quick-filters"
>
<section
class="header"
>
<section
class="left-actions"
>
<span
aria-label="filter"
class="anticon anticon-filter"
role="img"
>
<svg
aria-hidden="true"
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880.1 154H143.9c-24.5 0-39.8 26.7-27.5 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48zM603.4 798H420.6V642h182.9v156zm9.6-236.6l-9.5 16.6h-183l-9.5-16.6L212.7 226h598.6L613 561.4z"
/>
</svg>
</span>
<span
class="ant-typography text css-dev-only-do-not-override-2i2tap"
>
Filters for
</span>
<span
class="ant-typography sync-tag css-dev-only-do-not-override-2i2tap"
>
Test Query
</span>
</section>
<section
class="right-actions"
>
<span
aria-label="sync"
class="anticon anticon-sync sync-icon"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="sync"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 01755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8zm756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 01512.1 856a342.24 342.24 0 01-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 00-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 00-8-8.2z"
/>
</svg>
</span>
<div
class="divider-filter"
/>
<span
aria-label="vertical-align-top"
class="anticon anticon-vertical-align-top"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="vertical-align-top"
fill="currentColor"
focusable="false"
height="1em"
style="transform: rotate(270deg);"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M859.9 168H164.1c-4.5 0-8.1 3.6-8.1 8v60c0 4.4 3.6 8 8.1 8h695.8c4.5 0 8.1-3.6 8.1-8v-60c0-4.4-3.6-8-8.1-8zM518.3 355a8 8 0 00-12.6 0l-112 141.7a7.98 7.98 0 006.3 12.9h73.9V848c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V509.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 355z"
/>
</svg>
</span>
</section>
</section>
<div
class="overlay-scrollbar"
data-overlayscrollbars-initialize="true"
>
<div
data-overlayscrollbars-contents=""
>
<section
class="filters"
>
<div
class="checkbox-filter"
>
<section
class="filter-header-checkbox"
>
<section
class="left-action"
>
<svg
class="lucide lucide-chevron-down"
cursor="pointer"
fill="none"
height="13"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="13"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m6 9 6 6 6-6"
/>
</svg>
<span
class="ant-typography title css-dev-only-do-not-override-2i2tap"
>
Environment
</span>
</section>
<section
class="right-action"
>
<span
class="ant-typography clear-all css-dev-only-do-not-override-2i2tap"
>
Clear All
</span>
</section>
</section>
<section
class="search"
>
<input
class="ant-input css-dev-only-do-not-override-2i2tap"
placeholder="Filter values"
type="text"
value=""
/>
</section>
<section
class="values"
>
<div
class="value"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
>
<span
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
>
<input
checked=""
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<div
class="checkbox-value-section"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
>
mq-kafka
</span>
<button
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
type="button"
>
<span>
Only
</span>
</button>
<button
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
type="button"
>
<span>
Toggle
</span>
</button>
</div>
</div>
<div
class="value"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
>
<span
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
>
<input
checked=""
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<div
class="checkbox-value-section"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
>
otel-demo
</span>
<button
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
type="button"
>
<span>
Only
</span>
</button>
<button
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
type="button"
>
<span>
Toggle
</span>
</button>
</div>
</div>
<div
class="value"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
>
<span
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
>
<input
checked=""
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<div
class="checkbox-value-section"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
>
otlp-python
</span>
<button
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
type="button"
>
<span>
Only
</span>
</button>
<button
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
type="button"
>
<span>
Toggle
</span>
</button>
</div>
</div>
<div
class="value"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
>
<span
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
>
<input
checked=""
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<div
class="checkbox-value-section"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
>
sample-flask
</span>
<button
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
type="button"
>
<span>
Only
</span>
</button>
<button
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
type="button"
>
<span>
Toggle
</span>
</button>
</div>
</div>
</section>
</div>
<div
class="checkbox-filter"
>
<section
class="filter-header-checkbox"
>
<section
class="left-action"
>
<svg
class="lucide lucide-chevron-right"
cursor="pointer"
fill="none"
height="13"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="13"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m9 18 6-6-6-6"
/>
</svg>
<span
class="ant-typography title css-dev-only-do-not-override-2i2tap"
>
Service Name
</span>
</section>
<section
class="right-action"
/>
</section>
</div>
</section>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,30 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { FiltersType } from '../types';
export const QuickFiltersConfig = [
{
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'Service Name',
attributeKey: {
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: false,
},
];

View File

@@ -40,4 +40,5 @@ export enum QuickFiltersSource {
INFRA_MONITORING = 'infra-monitoring', INFRA_MONITORING = 'infra-monitoring',
TRACES_EXPLORER = 'traces-explorer', TRACES_EXPLORER = 'traces-explorer',
API_MONITORING = 'api-monitoring', API_MONITORING = 'api-monitoring',
EXCEPTIONS = 'exceptions',
} }

View File

@@ -8,6 +8,5 @@ export enum FeatureKeys {
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT', PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
ANOMALY_DETECTION = 'ANOMALY_DETECTION', ANOMALY_DETECTION = 'ANOMALY_DETECTION',
ONBOARDING_V3 = 'ONBOARDING_V3', ONBOARDING_V3 = 'ONBOARDING_V3',
THIRD_PARTY_API = 'THIRD_PARTY_API',
TRACE_FUNNELS = 'TRACE_FUNNELS', TRACE_FUNNELS = 'TRACE_FUNNELS',
} }

View File

@@ -27,4 +27,5 @@ export enum LOCALSTORAGE {
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS', CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING', DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING',
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS', METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
} }

View File

@@ -398,6 +398,23 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
], ],
}; };
export enum OperatorConfigKeys {
'EXCEPTIONS' = 'EXCEPTIONS',
}
export const OPERATORS_CONFIG = {
[OperatorConfigKeys.EXCEPTIONS]: [
OPERATORS['='],
OPERATORS['!='],
OPERATORS.IN,
OPERATORS.NIN,
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
OPERATORS.CONTAINS,
OPERATORS.NOT_CONTAINS,
],
};
export const HAVING_OPERATORS: string[] = [ export const HAVING_OPERATORS: string[] = [
OPERATORS['='], OPERATORS['='],
OPERATORS['!='], OPERATORS['!='],

View File

@@ -16,3 +16,51 @@ export const OperatorConversions: Array<{
traceValue: 'NotIn', traceValue: 'NotIn',
}, },
]; ];
// mapping from qb to exceptions
export const CompositeQueryOperatorsConfig: Array<{
label: string;
metricValue: string;
traceValue: OperatorValues;
}> = [
{
label: 'in',
metricValue: '=~',
traceValue: 'In',
},
{
label: 'nin',
metricValue: '!~',
traceValue: 'NotIn',
},
{
label: '=',
metricValue: '=',
traceValue: 'Equals',
},
{
label: '!=',
metricValue: '!=',
traceValue: 'NotEquals',
},
{
label: 'exists',
metricValue: '=~',
traceValue: 'Exists',
},
{
label: 'nexists',
metricValue: '!~',
traceValue: 'NotExists',
},
{
label: 'contains',
metricValue: '=~',
traceValue: 'Contains',
},
{
label: 'ncontains',
metricValue: '!~',
traceValue: 'NotContains',
},
];

View File

@@ -18,16 +18,17 @@ import getErrorCounts from 'api/errors/getErrorCounts';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useResourceAttribute from 'hooks/useResourceAttribute'; import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; import { convertCompositeQueryToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter'; import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams'; import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history'; import history from 'lib/history';
import { isUndefined } from 'lodash-es'; import { isUndefined } from 'lodash-es';
import { useTimezone } from 'providers/Timezone'; import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query'; import { useQueries } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@@ -109,10 +110,11 @@ function AllErrors(): JSX.Element {
); );
const { queries } = useResourceAttribute(); const { queries } = useResourceAttribute();
const compositeData = useGetCompositeQueryParam();
const [{ isLoading, data }, errorCountResponse] = useQueries([ const [{ isLoading, data }, errorCountResponse] = useQueries([
{ {
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries], queryKey: ['getAllErrors', updatedPath, maxTime, minTime, compositeData],
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
getAll({ getAll({
end: maxTime, end: maxTime,
@@ -123,7 +125,9 @@ function AllErrors(): JSX.Element {
orderParam: getUpdatedParams, orderParam: getUpdatedParams,
exceptionType: getUpdatedExceptionType, exceptionType: getUpdatedExceptionType,
serviceName: getUpdatedServiceName, serviceName: getUpdatedServiceName,
tags: convertRawQueriesToTraceSelectedTags(queries), tags: convertCompositeQueryToTraceSelectedTags(
compositeData?.builder.queryData?.[0]?.filters.items,
),
}), }),
enabled: !loading, enabled: !loading,
}, },
@@ -134,7 +138,7 @@ function AllErrors(): JSX.Element {
minTime, minTime,
getUpdatedExceptionType, getUpdatedExceptionType,
getUpdatedServiceName, getUpdatedServiceName,
queries, compositeData,
], ],
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> => queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
getErrorCounts({ getErrorCounts({
@@ -142,7 +146,9 @@ function AllErrors(): JSX.Element {
start: minTime, start: minTime,
exceptionType: getUpdatedExceptionType, exceptionType: getUpdatedExceptionType,
serviceName: getUpdatedServiceName, serviceName: getUpdatedServiceName,
tags: convertRawQueriesToTraceSelectedTags(queries), tags: convertCompositeQueryToTraceSelectedTags(
compositeData?.builder.queryData?.[0]?.filters.items,
),
}), }),
enabled: !loading, enabled: !loading,
}, },
@@ -429,12 +435,8 @@ function AllErrors(): JSX.Element {
[pathname], [pathname],
); );
const logEventCalledRef = useRef(false);
useEffect(() => { useEffect(() => {
if ( if (!isUndefined(errorCountResponse.data?.payload)) {
!logEventCalledRef.current &&
!isUndefined(errorCountResponse.data?.payload)
) {
const selectedEnvironments = queries.find( const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment', (val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue; )?.tagValue;
@@ -442,9 +444,12 @@ function AllErrors(): JSX.Element {
logEvent('Exception: List page visited', { logEvent('Exception: List page visited', {
numberOfExceptions: errorCountResponse?.data?.payload, numberOfExceptions: errorCountResponse?.data?.payload,
selectedEnvironments, selectedEnvironments,
resourceAttributeUsed: !!queries?.length, resourceAttributeUsed: !!compositeData?.builder.queryData?.[0]?.filters
.items?.length,
tags: convertCompositeQueryToTraceSelectedTags(
compositeData?.builder.queryData?.[0]?.filters.items,
),
}); });
logEventCalledRef.current = true;
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [errorCountResponse.data?.payload]); }, [errorCountResponse.data?.payload]);

View File

@@ -0,0 +1,114 @@
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import TimezoneProvider from 'providers/Timezone';
import { Provider, useSelector } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
import AllErrors from '../index';
import {
INIT_URL_WITH_COMMON_QUERY,
MOCK_ERROR_LIST,
TAG_FROM_QUERY,
} from './constants';
jest.mock('hooks/useResourceAttribute', () =>
jest.fn(() => ({
queries: [],
})),
);
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}));
function Exceptions({ initUrl }: { initUrl?: string[] }): JSX.Element {
return (
<MemoryRouter initialEntries={initUrl ?? ['/exceptions']}>
<TimezoneProvider>
<Provider store={store}>
<MockQueryClientProvider>
<AllErrors />
</MockQueryClientProvider>
</Provider>
</TimezoneProvider>
</MemoryRouter>
);
}
Exceptions.defaultProps = {
initUrl: ['/exceptions'],
};
const BASE_URL = ENVIRONMENT.baseURL;
const listErrorsURL = `${BASE_URL}/api/v1/listErrors`;
const countErrorsURL = `${BASE_URL}/api/v1/countErrors`;
const postListErrorsSpy = jest.fn();
describe('Exceptions - All Errors', () => {
beforeEach(() => {
(useSelector as jest.Mock).mockReturnValue({
maxTime: 1000,
minTime: 0,
loading: false,
});
server.use(
rest.post(listErrorsURL, async (req, res, ctx) => {
const body = await req.json();
postListErrorsSpy(body);
return res(ctx.status(200), ctx.json(MOCK_ERROR_LIST));
}),
);
server.use(
rest.post(countErrorsURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(540)),
),
);
});
it('renders correctly with default props', async () => {
render(<Exceptions />);
const item = await screen.findByText(/redis timeout/i);
expect(item).toBeInTheDocument();
});
it('should sort Error Message appropriately', async () => {
render(<Exceptions />);
await screen.findByText(/redis timeout/i);
const caretIconUp = screen.getAllByLabelText('caret-up')[0];
const caretIconDown = screen.getAllByLabelText('caret-down')[0];
// sort by ascending
expect(caretIconUp.className).not.toContain('active');
fireEvent.click(caretIconUp);
expect(caretIconUp.className).toContain('active');
let queryParams = new URLSearchParams(window.location.search);
expect(queryParams.get('order')).toBe('ascending');
expect(queryParams.get('orderParam')).toBe('exceptionType');
// sort by descending
expect(caretIconDown.className).not.toContain('active');
fireEvent.click(caretIconDown);
expect(caretIconDown.className).toContain('active');
queryParams = new URLSearchParams(window.location.search);
expect(queryParams.get('order')).toBe('descending');
});
it('should call useQueries with exact composite query object', async () => {
render(<Exceptions initUrl={[INIT_URL_WITH_COMMON_QUERY]} />);
await screen.findByText(/redis timeout/i);
expect(postListErrorsSpy).toHaveBeenCalledWith(
expect.objectContaining({
tags: TAG_FROM_QUERY,
}),
);
});
});

View File

@@ -0,0 +1,94 @@
export const MOCK_USE_QUERIES_DATA = [
{
isLoading: false,
isError: false,
error: null,
data: {
statusCode: 200,
payload: [
{
exceptionType: '*errors.errorString',
exceptionMessage: 'redis timeout',
exceptionCount: 2510,
lastSeen: '2025-04-14T18:27:57.797616374Z',
firstSeen: '2025-04-14T17:58:00.262775497Z',
serviceName: 'redis-manual',
groupID: '511b9c91a92b9c5166ecb77235f5743b',
},
],
},
},
{
status: 'success',
isLoading: false,
isSuccess: true,
isError: false,
isIdle: false,
data: {
statusCode: 200,
error: null,
payload: 525,
},
dataUpdatedAt: 1744661020341,
error: null,
errorUpdatedAt: 0,
failureCount: 0,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isRefetching: false,
isLoadingError: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isStale: true,
},
];
export const INIT_URL_WITH_COMMON_QUERY =
'/exceptions?compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522dataSource%2522%253A%2522traces%2522%252C%2522queryName%2522%253A%2522A%2522%252C%2522aggregateOperator%2522%253A%2522noop%2522%252C%2522aggregateAttribute%2522%253A%257B%2522id%2522%253A%2522----resource--false%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522key%2522%253A%2522%2522%252C%2522isColumn%2522%253Afalse%252C%2522type%2522%253A%2522resource%2522%252C%2522isJSON%2522%253Afalse%257D%252C%2522timeAggregation%2522%253A%2522rate%2522%252C%2522spaceAggregation%2522%253A%2522sum%2522%252C%2522functions%2522%253A%255B%255D%252C%2522filters%2522%253A%257B%2522items%2522%253A%255B%257B%2522id%2522%253A%2522db118ac7-9313-4adb-963f-f31b5b32c496%2522%252C%2522op%2522%253A%2522in%2522%252C%2522key%2522%253A%257B%2522key%2522%253A%2522deployment.environment%2522%252C%2522dataType%2522%253A%2522string%2522%252C%2522type%2522%253A%2522resource%2522%252C%2522isColumn%2522%253Afalse%252C%2522isJSON%2522%253Afalse%257D%252C%2522value%2522%253A%2522mq-kafka%2522%257D%255D%252C%2522op%2522%253A%2522AND%2522%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522stepInterval%2522%253A60%252C%2522having%2522%253A%255B%255D%252C%2522limit%2522%253Anull%252C%2522orderBy%2522%253A%255B%255D%252C%2522groupBy%2522%253A%255B%255D%252C%2522legend%2522%253A%2522%2522%252C%2522reduceTo%2522%253A%2522avg%2522%257D%255D%252C%2522queryFormulas%2522%253A%255B%255D%257D%252C%2522promql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522query%2522%253A%2522%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%257D%255D%252C%2522clickhouse_sql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%252C%2522query%2522%253A%2522%2522%257D%255D%252C%2522id%2522%253A%2522dd576d04-0822-476d-b0c2-807a7af2e5e7%2522%257D';
export const extractCompositeQueryObject = (
url: string,
): Record<string, unknown> | null => {
try {
const urlObj = new URL(`http://dummy-base${url}`); // Add dummy base to parse relative URL
const encodedParam = urlObj.searchParams.get('compositeQuery');
if (!encodedParam) return null;
// Decode twice
const firstDecode = decodeURIComponent(encodedParam);
const secondDecode = decodeURIComponent(firstDecode);
// Parse JSON
return JSON.parse(secondDecode);
} catch (err) {
console.error('Failed to extract compositeQuery:', err);
return null;
}
};
export const TAG_FROM_QUERY = [
{
BoolValues: [],
Key: 'deployment.environment',
NumberValues: [],
Operator: 'In',
StringValues: ['mq-kafka'],
TagType: 'ResourceAttribute',
},
];
export const MOCK_ERROR_LIST = [
{
exceptionType: '*errors.errorString',
exceptionMessage: 'redis timeout',
exceptionCount: 2510,
lastSeen: '2025-04-14T18:27:57.797616374Z',
firstSeen: '2025-04-14T17:58:00.262775497Z',
serviceName: 'redis-manual',
groupID: '511b9c91a92b9c5166ecb77235f5743b',
},
];

View File

@@ -339,6 +339,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING'; const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
const isExceptionsView = (): boolean => routeKey === 'ALL_ERROR';
const isTracesView = (): boolean => const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS'; routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
@@ -661,7 +663,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isMessagingQueues() || isMessagingQueues() ||
isCloudIntegrationPage() || isCloudIntegrationPage() ||
isInfraMonitoring() || isInfraMonitoring() ||
isApiMonitoringView() isApiMonitoringView() ||
isExceptionsView()
? 0 ? 0
: '0 1rem', : '0 1rem',

View File

@@ -333,7 +333,7 @@ export default function BillingContainer(): JSX.Element {
}); });
updateCreditCard({ updateCreditCard({
url: window.location.href, url: window.location.origin,
}); });
} else { } else {
logEvent('Billing : Manage Billing', { logEvent('Billing : Manage Billing', {
@@ -342,7 +342,7 @@ export default function BillingContainer(): JSX.Element {
}); });
manageCreditCard({ manageCreditCard({
url: window.location.href, url: window.location.origin,
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -121,7 +121,7 @@ function CreateRules(): JSX.Element {
alertType={alertType} alertType={alertType}
formInstance={formInstance} formInstance={formInstance}
initialValue={initValues} initialValue={initValues}
ruleId={0} ruleId=""
/> />
); );
} }

View File

@@ -22,7 +22,7 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
interface EditRulesProps { interface EditRulesProps {
initialValue: AlertDef; initialValue: AlertDef;
ruleId: number; ruleId: string;
} }
export default EditRules; export default EditRules;

View File

@@ -11,6 +11,7 @@ import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilder } from 'container/QueryBuilder';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { isEmpty } from 'lodash-es';
import { Atom, Play, Terminal } from 'lucide-react'; import { Atom, Play, Terminal } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -156,7 +157,7 @@ function QuerySection({
runQuery(); runQuery();
logEvent('Alert: Stage and run query', { logEvent('Alert: Stage and run query', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertType], dataSource: ALERTS_DATA_SOURCE_MAP[alertType],
isNewRule: !ruleId || ruleId === 0, isNewRule: !ruleId || isEmpty(ruleId),
ruleId, ruleId,
queryType: queryCategory, queryType: queryCategory,
}); });
@@ -230,7 +231,7 @@ interface QuerySectionProps {
runQuery: VoidFunction; runQuery: VoidFunction;
alertDef: AlertDef; alertDef: AlertDef;
panelType: PANEL_TYPES; panelType: PANEL_TYPES;
ruleId: number; ruleId: string;
} }
export default QuerySection; export default QuerySection;

View File

@@ -21,7 +21,7 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi'; import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEqual } from 'lodash-es'; import { isEmpty, isEqual } from 'lodash-es';
import { BellDot, ExternalLink } from 'lucide-react'; import { BellDot, ExternalLink } from 'lucide-react';
import Tabs2 from 'periscope/components/Tabs2'; import Tabs2 from 'periscope/components/Tabs2';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
@@ -121,7 +121,7 @@ function FormAlertRules({
// use query client // use query client
const ruleCache = useQueryClient(); const ruleCache = useQueryClient();
const isNewRule = ruleId === 0; const isNewRule = !ruleId || isEmpty(ruleId);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [queryStatus, setQueryStatus] = useState<string>(''); const [queryStatus, setQueryStatus] = useState<string>('');
@@ -481,7 +481,7 @@ function FormAlertRules({
try { try {
const apiReq = const apiReq =
ruleId && ruleId > 0 ruleId && !isEmpty(ruleId)
? { data: postableAlert, id: ruleId } ? { data: postableAlert, id: ruleId }
: { data: postableAlert }; : { data: postableAlert };
@@ -491,7 +491,7 @@ function FormAlertRules({
logData = { logData = {
status: 'success', status: 'success',
statusMessage: statusMessage:
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'), !ruleId || isEmpty(ruleId) ? t('rule_created') : t('rule_edited'),
}; };
notifications.success({ notifications.success({
@@ -543,7 +543,7 @@ function FormAlertRules({
dataSource: ALERTS_DATA_SOURCE_MAP[postableAlert?.alertType as AlertTypes], dataSource: ALERTS_DATA_SOURCE_MAP[postableAlert?.alertType as AlertTypes],
channelNames: postableAlert?.preferredChannels, channelNames: postableAlert?.preferredChannels,
broadcastToAll: postableAlert?.broadcastToAll, broadcastToAll: postableAlert?.broadcastToAll,
isNewRule: !ruleId || ruleId === 0, isNewRule: !ruleId || isEmpty(ruleId),
ruleId, ruleId,
queryType: currentQuery.queryType, queryType: currentQuery.queryType,
alertId: postableAlert?.id, alertId: postableAlert?.id,
@@ -628,7 +628,7 @@ function FormAlertRules({
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes], dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
channelNames: postableAlert?.preferredChannels, channelNames: postableAlert?.preferredChannels,
broadcastToAll: postableAlert?.broadcastToAll, broadcastToAll: postableAlert?.broadcastToAll,
isNewRule: !ruleId || ruleId === 0, isNewRule: !ruleId || isEmpty(ruleId),
ruleId, ruleId,
queryType: currentQuery.queryType, queryType: currentQuery.queryType,
status: statusResponse.status, status: statusResponse.status,
@@ -700,7 +700,7 @@ function FormAlertRules({
alertDef?.broadcastToAll || alertDef?.broadcastToAll ||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0); (alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
const isRuleCreated = !ruleId || ruleId === 0; const isRuleCreated = !ruleId || isEmpty(ruleId);
function handleRedirection(option: AlertTypes): void { function handleRedirection(option: AlertTypes): void {
let url; let url;
@@ -716,7 +716,7 @@ function FormAlertRules({
if (url) { if (url) {
logEvent('Alert: Check example alert clicked', { logEvent('Alert: Check example alert clicked', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes], dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
isNewRule: !ruleId || ruleId === 0, isNewRule: !ruleId || isEmpty(ruleId),
ruleId, ruleId,
queryType: currentQuery.queryType, queryType: currentQuery.queryType,
link: url, link: url,
@@ -881,8 +881,8 @@ function FormAlertRules({
type="default" type="default"
onClick={onCancelHandler} onClick={onCancelHandler}
> >
{ruleId === 0 && t('button_cancelchanges')} {(!ruleId || isEmpty(ruleId)) && t('button_cancelchanges')}
{ruleId > 0 && t('button_discard')} {ruleId && !isEmpty(ruleId) && t('button_discard')}
</ActionButton> </ActionButton>
</ButtonContainer> </ButtonContainer>
</MainFormContainer> </MainFormContainer>
@@ -899,7 +899,7 @@ interface FormAlertRuleProps {
alertType?: AlertTypes; alertType?: AlertTypes;
formInstance: FormInstance; formInstance: FormInstance;
initialValue: AlertDef; initialValue: AlertDef;
ruleId: number; ruleId: string;
} }
export default FormAlertRules; export default FormAlertRules;

View File

@@ -153,7 +153,9 @@ export default function AlertRules({
<div className="alert-rule-item-name-container home-data-item-name-container"> <div className="alert-rule-item-name-container home-data-item-name-container">
<img <img
src={ src={
rule.id % 2 === 0 ? '/Icons/eight-ball.svg' : '/Icons/circus-tent.svg' Math.random() % 2 === 0
? '/Icons/eight-ball.svg'
: '/Icons/circus-tent.svg'
} }
alt="alert-rules" alt="alert-rules"
className="alert-rules-img" className="alert-rules-img"

View File

@@ -2,7 +2,7 @@
import './Home.styles.scss'; import './Home.styles.scss';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Button, Popover } from 'antd'; import { Alert, Button, Popover } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { HostListPayload } from 'api/infraMonitoring/getHostLists'; import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList'; import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
@@ -644,6 +644,16 @@ export default function Home(): JSX.Element {
</div> </div>
<div className="home-right-content"> <div className="home-right-content">
<div className="home-notifications-container">
<div className="notification">
<Alert
message="We're transitioning alert rule IDs from integers to UUIDs on April 23, 2025. Both old and new alert links will continue to work after this change - existing notifications using integer IDs will remain functional while new alerts will use the UUID format."
type="info"
showIcon
/>
</div>
</div>
{!isWelcomeChecklistSkipped && !loadingUserPreferences && ( {!isWelcomeChecklistSkipped && !loadingUserPreferences && (
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
<Card className="checklist-card"> <Card className="checklist-card">

View File

@@ -24,7 +24,7 @@ function DeleteAlert({
const defaultErrorMessage = 'Something went wrong'; const defaultErrorMessage = 'Something went wrong';
const onDeleteHandler = async (id: number): Promise<void> => { const onDeleteHandler = async (id: string): Promise<void> => {
try { try {
const response = await deleteAlerts({ const response = await deleteAlerts({
id, id,

View File

@@ -25,7 +25,7 @@ function ToggleAlertState({
const defaultErrorMessage = 'Something went wrong'; const defaultErrorMessage = 'Something went wrong';
const onToggleHandler = async ( const onToggleHandler = async (
id: number, id: string,
disabled: boolean, disabled: boolean,
): Promise<void> => { ): Promise<void> => {
try { try {

View File

@@ -13,6 +13,7 @@ import AddToQueryHOC, {
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import { OPERATORS } from 'constants/queryBuilder'; import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types'; import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history'; import history from 'lib/history';
@@ -34,9 +35,6 @@ import FieldRenderer from './FieldRenderer';
import { TableViewActions } from './TableView/TableViewActions'; import { TableViewActions } from './TableView/TableViewActions';
import { filterKeyForField, findKeyPath, flattenObject } from './utils'; import { filterKeyForField, findKeyPath, flattenObject } from './utils';
// Fields which should be restricted from adding it to query
const RESTRICTED_FIELDS = ['timestamp'];
interface TableViewProps { interface TableViewProps {
logData: ILog; logData: ILog;
fieldSearchInput: string; fieldSearchInput: string;
@@ -249,7 +247,7 @@ function TableView({
} }
const fieldFilterKey = filterKeyForField(field); const fieldFilterKey = filterKeyForField(field);
if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) { if (!RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey)) {
return ( return (
<AddToQueryHOC <AddToQueryHOC
fieldKey={fieldFilterKey} fieldKey={fieldFilterKey}

View File

@@ -9,6 +9,7 @@ import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { OPERATORS } from 'constants/queryBuilder'; import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react'; import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
@@ -142,7 +143,7 @@ export function TableViewActions(
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}> <CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
{renderFieldContent()} {renderFieldContent()}
</CopyClipboardHOC> </CopyClipboardHOC>
{!isListViewPanel && ( {!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
<span className="action-btn"> <span className="action-btn">
<Tooltip title="Filter for value"> <Tooltip title="Filter for value">
<Button <Button

View File

@@ -0,0 +1,130 @@
import { render, screen } from '@testing-library/react';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { TableViewActions } from '../TableViewActions';
// Mock the components and hooks
jest.mock('components/Logs/CopyClipboardHOC', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }): JSX.Element => (
<div className="CopyClipboardHOC">{children}</div>
),
}));
jest.mock('providers/Timezone', () => ({
useTimezone: (): {
formatTimezoneAdjustedTimestamp: (timestamp: string) => string;
} => ({
formatTimezoneAdjustedTimestamp: (timestamp: string): string => timestamp,
}),
}));
jest.mock('react-router-dom', () => ({
useLocation: (): {
pathname: string;
search: string;
hash: string;
state: null;
} => ({
pathname: '/test',
search: '',
hash: '',
state: null,
}),
}));
describe('TableViewActions', () => {
const TEST_VALUE = 'test value';
const ACTION_BUTTON_TEST_ID = '.action-btn';
const defaultProps = {
fieldData: {
field: 'test-field',
value: TEST_VALUE,
},
record: {
key: 'test-key',
field: 'test-field',
value: TEST_VALUE,
},
isListViewPanel: false,
isfilterInLoading: false,
isfilterOutLoading: false,
onClickHandler: jest.fn(),
onGroupByAttribute: jest.fn(),
};
it('should render without crashing', () => {
render(
<TableViewActions
fieldData={defaultProps.fieldData}
record={defaultProps.record}
isListViewPanel={defaultProps.isListViewPanel}
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
/>,
);
expect(screen.getByText(TEST_VALUE)).toBeInTheDocument();
});
it('should not render action buttons for restricted fields', () => {
RESTRICTED_SELECTED_FIELDS.forEach((field) => {
const { container } = render(
<TableViewActions
fieldData={{
...defaultProps.fieldData,
field,
}}
record={{
...defaultProps.record,
field,
}}
isListViewPanel={defaultProps.isListViewPanel}
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
/>,
);
// Verify that action buttons are not rendered for restricted fields
expect(
container.querySelector(ACTION_BUTTON_TEST_ID),
).not.toBeInTheDocument();
});
});
it('should render action buttons for non-restricted fields', () => {
const { container } = render(
<TableViewActions
fieldData={defaultProps.fieldData}
record={defaultProps.record}
isListViewPanel={defaultProps.isListViewPanel}
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
/>,
);
// Verify that action buttons are rendered for non-restricted fields
expect(container.querySelector(ACTION_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should not render action buttons in list view panel', () => {
const { container } = render(
<TableViewActions
fieldData={defaultProps.fieldData}
record={defaultProps.record}
isListViewPanel
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
/>,
);
// Verify that action buttons are not rendered in list view panel
expect(
container.querySelector(ACTION_BUTTON_TEST_ID),
).not.toBeInTheDocument();
});
});

View File

@@ -138,7 +138,7 @@ export const deleteDowntimeHandler = ({
export const createEditDowntimeSchedule = async ( export const createEditDowntimeSchedule = async (
props: DowntimeScheduleUpdatePayload, props: DowntimeScheduleUpdatePayload,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => { ): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
if (props.id && props.id > 0) { if (props.id) {
return updateDowntimeSchedule({ ...props }); return updateDowntimeSchedule({ ...props });
} }
return createDowntimeSchedule({ ...props.data }); return createDowntimeSchedule({ ...props.data });

View File

@@ -453,7 +453,7 @@ export const Query = memo(function Query({
</Col> </Col>
)} )}
<Col flex="1" className="qb-search-container"> <Col flex="1" className="qb-search-container">
{query.dataSource === DataSource.LOGS ? ( {[DataSource.LOGS, DataSource.TRACES].includes(query.dataSource) ? (
<QueryBuilderSearchV2 <QueryBuilderSearchV2
query={query} query={query}
onChange={handleChangeTagFilters} onChange={handleChangeTagFilters}

View File

@@ -56,7 +56,6 @@ import { PLACEHOLDER } from './constant';
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs'; import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
import OptionRenderer from './OptionRenderer'; import OptionRenderer from './OptionRenderer';
import OptionRendererForLogs from './OptionRendererForLogs'; import OptionRendererForLogs from './OptionRendererForLogs';
import SpanScopeSelector from './SpanScopeSelector';
import { StyledCheckOutlined, TypographyText } from './style'; import { StyledCheckOutlined, TypographyText } from './style';
import { import {
convertExampleQueriesToOptions, convertExampleQueriesToOptions,
@@ -84,11 +83,6 @@ function QueryBuilderSearch({
pathname, pathname,
]); ]);
const isTracesExplorerPage = useMemo(
() => pathname === ROUTES.TRACES_EXPLORER,
[pathname],
);
const [isEditingTag, setIsEditingTag] = useState(false); const [isEditingTag, setIsEditingTag] = useState(false);
const { const {
@@ -489,7 +483,6 @@ function QueryBuilderSearch({
</Select.Option> </Select.Option>
))} ))}
</Select> </Select>
{isTracesExplorerPage && <SpanScopeSelector queryName={query.queryName} />}
</div> </div>
); );
} }

View File

@@ -2,6 +2,7 @@
import './QueryBuilderSearchV2.styles.scss'; import './QueryBuilderSearchV2.styles.scss';
import { Typography } from 'antd'; import { Typography } from 'antd';
import cx from 'classnames';
import { import {
ArrowDown, ArrowDown,
ArrowUp, ArrowUp,
@@ -25,6 +26,7 @@ interface ICustomDropdownProps {
exampleQueries: TagFilter[]; exampleQueries: TagFilter[];
onChange: (value: TagFilter) => void; onChange: (value: TagFilter) => void;
currentFilterItem?: ITag; currentFilterItem?: ITag;
isLogsDataSource: boolean;
} }
export default function QueryBuilderSearchDropdown( export default function QueryBuilderSearchDropdown(
@@ -38,11 +40,14 @@ export default function QueryBuilderSearchDropdown(
exampleQueries, exampleQueries,
options, options,
onChange, onChange,
isLogsDataSource,
} = props; } = props;
const userOs = getUserOperatingSystem(); const userOs = getUserOperatingSystem();
return ( return (
<> <>
<div className="content"> <div
className={cx('content', { 'non-logs-data-source': !isLogsDataSource })}
>
{!currentFilterItem?.key ? ( {!currentFilterItem?.key ? (
<div className="suggested-filters">Suggested Filters</div> <div className="suggested-filters">Suggested Filters</div>
) : !currentFilterItem?.op ? ( ) : !currentFilterItem?.op ? (

View File

@@ -11,6 +11,11 @@
.rc-virtual-list-holder { .rc-virtual-list-holder {
height: 115px; height: 115px;
} }
&.non-logs-data-source {
.rc-virtual-list-holder {
height: 256px;
}
}
} }
} }

View File

@@ -5,6 +5,7 @@ import { Select, Spin, Tag, Tooltip } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { import {
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY, DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
OperatorConfigKeys,
OPERATORS, OPERATORS,
QUERY_BUILDER_OPERATORS_BY_TYPES, QUERY_BUILDER_OPERATORS_BY_TYPES,
QUERY_BUILDER_SEARCH_VALUES, QUERY_BUILDER_SEARCH_VALUES,
@@ -62,7 +63,9 @@ import {
getTagToken, getTagToken,
isInNInOperator, isInNInOperator,
} from '../QueryBuilderSearch/utils'; } from '../QueryBuilderSearch/utils';
import { filterByOperatorConfig } from '../utils';
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown'; import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
import SpanScopeSelector from './SpanScopeSelector';
import Suggestions from './Suggestions'; import Suggestions from './Suggestions';
export interface ITag { export interface ITag {
@@ -88,6 +91,7 @@ interface QueryBuilderSearchV2Props {
className?: string; className?: string;
suffixIcon?: React.ReactNode; suffixIcon?: React.ReactNode;
hardcodedAttributeKeys?: BaseAutocompleteData[]; hardcodedAttributeKeys?: BaseAutocompleteData[];
operatorConfigKey?: OperatorConfigKeys;
} }
export interface Option { export interface Option {
@@ -121,6 +125,7 @@ function QueryBuilderSearchV2(
suffixIcon, suffixIcon,
whereClauseConfig, whereClauseConfig,
hardcodedAttributeKeys, hardcodedAttributeKeys,
operatorConfigKey,
} = props; } = props;
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
@@ -290,7 +295,8 @@ function QueryBuilderSearchV2(
if ( if (
isObject(parsedValue) && isObject(parsedValue) &&
parsedValue?.key && parsedValue?.key &&
parsedValue?.key?.split(' ').length > 1 parsedValue?.key?.split(' ').length > 1 &&
isLogsDataSource
) { ) {
setTags((prev) => [ setTags((prev) => [
...prev, ...prev,
@@ -405,7 +411,13 @@ function QueryBuilderSearchV2(
} }
} }
}, },
[currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue], [
currentFilterItem?.key,
currentFilterItem?.op,
currentState,
isLogsDataSource,
searchValue,
],
); );
const handleSearch = useCallback((value: string) => { const handleSearch = useCallback((value: string) => {
@@ -689,12 +701,29 @@ function QueryBuilderSearchV2(
})), })),
); );
} else { } else {
setDropdownOptions( setDropdownOptions([
data?.payload?.attributeKeys?.map((key) => ({ // Add user typed option if it doesn't exist in the payload
...(tagKey.trim().length > 0 &&
!data?.payload?.attributeKeys?.some((val) => val.key === tagKey)
? [
{
label: tagKey,
value: {
key: tagKey,
dataType: DataTypes.EMPTY,
type: '',
isColumn: false,
isJSON: false,
},
},
]
: []),
// Map existing attribute keys from payload
...(data?.payload?.attributeKeys?.map((key) => ({
label: key.key, label: key.key,
value: key, value: key,
})) || [], })) || []),
); ]);
} }
} }
if (currentState === DropdownState.OPERATOR) { if (currentState === DropdownState.OPERATOR) {
@@ -717,15 +746,11 @@ function QueryBuilderSearchV2(
op.label.startsWith(partialOperator.toLocaleUpperCase()), op.label.startsWith(partialOperator.toLocaleUpperCase()),
); );
} }
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) { } else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({ operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
label: operator, label: operator,
value: operator, value: operator,
})); }));
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
} else { } else {
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map( operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
(operator) => ({ (operator) => ({
@@ -739,9 +764,12 @@ function QueryBuilderSearchV2(
op.label.startsWith(partialOperator.toLocaleUpperCase()), op.label.startsWith(partialOperator.toLocaleUpperCase()),
); );
} }
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
} }
const filterOperatorOptions = filterByOperatorConfig(
operatorOptions,
operatorConfigKey,
);
setDropdownOptions([{ label: '', value: '' }, ...filterOperatorOptions]);
} }
if (currentState === DropdownState.ATTRIBUTE_VALUE) { if (currentState === DropdownState.ATTRIBUTE_VALUE) {
@@ -774,6 +802,7 @@ function QueryBuilderSearchV2(
isLogsDataSource, isLogsDataSource,
searchValue, searchValue,
suggestionsData?.payload?.attributes, suggestionsData?.payload?.attributes,
operatorConfigKey,
]); ]);
// keep the query in sync with the selected tags in logs explorer page // keep the query in sync with the selected tags in logs explorer page
@@ -907,6 +936,11 @@ function QueryBuilderSearchV2(
); );
}; };
const isTracesDataSource = useMemo(
() => query.dataSource === DataSource.TRACES,
[query.dataSource],
);
return ( return (
<div className="query-builder-search-v2"> <div className="query-builder-search-v2">
<Select <Select
@@ -964,6 +998,7 @@ function QueryBuilderSearchV2(
exampleQueries={suggestionsData?.payload?.example_queries || []} exampleQueries={suggestionsData?.payload?.example_queries || []}
tags={tags} tags={tags}
currentFilterItem={currentFilterItem} currentFilterItem={currentFilterItem}
isLogsDataSource={isLogsDataSource}
/> />
)} )}
> >
@@ -990,6 +1025,7 @@ function QueryBuilderSearchV2(
); );
})} })}
</Select> </Select>
{isTracesDataSource && <SpanScopeSelector queryName={query.queryName} />}
</div> </div>
); );
} }
@@ -1000,6 +1036,7 @@ QueryBuilderSearchV2.defaultProps = {
suffixIcon: null, suffixIcon: null,
whereClauseConfig: {}, whereClauseConfig: {},
hardcodedAttributeKeys: undefined, hardcodedAttributeKeys: undefined,
operatorConfigKey: undefined,
}; };
export default QueryBuilderSearchV2; export default QueryBuilderSearchV2;

View File

@@ -120,6 +120,7 @@ function SpanScopeSelector({ queryName }: SpanScopeSelectorProps): JSX.Element {
<Select <Select
value={selectedScope} value={selectedScope}
className="span-scope-selector" className="span-scope-selector"
data-testid="span-scope-selector"
onChange={handleScopeChange} onChange={handleScopeChange}
options={SELECT_OPTIONS} options={SELECT_OPTIONS}
/> />

View File

@@ -0,0 +1,196 @@
/* eslint-disable react/jsx-props-no-spreading */
import {
act,
fireEvent,
render,
RenderResult,
screen,
} from '@testing-library/react';
import {
initialQueriesMap,
initialQueryBuilderFormValues,
} from 'constants/queryBuilder';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { QueryClient, QueryClientProvider } from 'react-query';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import QueryBuilderSearchV2 from '../QueryBuilderSearchV2';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
describe('Span scope selector', () => {
it('should render span scope selector when data source is TRACES', () => {
const { getByTestId } = render(
<QueryClientProvider client={queryClient}>
<QueryBuilderSearchV2
query={{
...initialQueryBuilderFormValues,
dataSource: DataSource.TRACES,
}}
onChange={jest.fn()}
/>
</QueryClientProvider>,
);
expect(getByTestId('span-scope-selector')).toBeInTheDocument();
});
it('should not render span scope selector for non-TRACES data sources', () => {
const { queryByTestId } = render(
<QueryClientProvider client={queryClient}>
<QueryBuilderSearchV2
query={{
...initialQueryBuilderFormValues,
dataSource: DataSource.METRICS,
}}
onChange={jest.fn()}
/>
</QueryClientProvider>,
);
expect(queryByTestId('span-scope-selector')).not.toBeInTheDocument();
});
});
const mockOnChange = jest.fn();
const mockHandleRunQuery = jest.fn();
const defaultProps = {
query: {
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
queryName: 'traces_query',
},
onChange: mockOnChange,
};
const renderWithContext = (props = {}): RenderResult => {
const mergedProps = { ...defaultProps, ...props };
return render(
<QueryClientProvider client={queryClient}>
<QueryBuilderContext.Provider
value={
{
currentQuery: initialQueriesMap.traces,
handleRunQuery: mockHandleRunQuery,
} as any
}
>
<QueryBuilderSearchV2 {...mergedProps} />
</QueryBuilderContext.Provider>
</QueryClientProvider>,
);
};
const mockAggregateKeysData = {
payload: {
attributeKeys: [
{
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'http.status',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: 'http.status--string--tag--false',
},
],
},
};
jest.mock('hooks/queryBuilder/useGetAggregateKeys', () => ({
useGetAggregateKeys: jest.fn(() => ({
data: mockAggregateKeysData,
isFetching: false,
})),
}));
const mockAggregateValuesData = {
payload: {
stringAttributeValues: ['200', '404', '500'],
numberAttributeValues: [200, 404, 500],
},
};
jest.mock('hooks/queryBuilder/useGetAggregateValues', () => ({
useGetAggregateValues: jest.fn(() => ({
data: mockAggregateValuesData,
isFetching: false,
})),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('Suggestion Key -> Operator -> Value Flow', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should complete full flow from key selection to value', async () => {
const { container } = renderWithContext();
// Get the combobox input specifically
const combobox = container.querySelector(
'.query-builder-search-v2 .ant-select-selection-search-input',
) as HTMLInputElement;
// 1. Focus and type to trigger key suggestions
await act(async () => {
fireEvent.focus(combobox);
fireEvent.change(combobox, { target: { value: 'http.' } });
});
// Wait for dropdown to appear
await screen.findByRole('listbox');
// 2. Select a key from suggestions
const statusOption = await screen.findByText('http.status');
await act(async () => {
fireEvent.click(statusOption);
});
// Should show operator suggestions
expect(screen.getByText('=')).toBeInTheDocument();
expect(screen.getByText('!=')).toBeInTheDocument();
// 3. Select an operator
const equalsOption = screen.getByText('=');
await act(async () => {
fireEvent.click(equalsOption);
});
// Should show value suggestions
expect(screen.getByText('200')).toBeInTheDocument();
expect(screen.getByText('404')).toBeInTheDocument();
expect(screen.getByText('500')).toBeInTheDocument();
// 4. Select a value
const valueOption = screen.getByText('200');
await act(async () => {
fireEvent.click(valueOption);
});
// Verify final filter
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: 'http.status' }),
op: '=',
value: '200',
}),
]),
}),
);
});
});

View File

@@ -0,0 +1,165 @@
import {
fireEvent,
render,
RenderResult,
screen,
} from '@testing-library/react';
import { initialQueriesMap } from 'constants/queryBuilder';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import SpanScopeSelector from '../SpanScopeSelector';
const mockRedirectWithQueryBuilderData = jest.fn();
// Helper to create filter items
const createSpanScopeFilter = (key: string): TagFilterItem => ({
id: 'span-filter',
key: {
key,
type: 'spanSearchScope',
},
op: '=',
value: 'true',
});
const defaultQuery = {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
queryName: 'A',
},
],
},
};
// Helper to create query with filters
const createQueryWithFilters = (filters: TagFilterItem[]): Query => ({
...defaultQuery,
builder: {
...defaultQuery.builder,
queryData: [
{
...defaultQuery.builder.queryData[0],
filters: {
items: filters,
op: 'AND',
},
},
],
},
});
const renderWithContext = (
queryName = 'A',
initialQuery = defaultQuery,
): RenderResult =>
render(
<QueryBuilderContext.Provider
value={
{
currentQuery: initialQuery,
redirectWithQueryBuilderData: mockRedirectWithQueryBuilderData,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any
}
>
<SpanScopeSelector queryName={queryName} />
</QueryBuilderContext.Provider>,
);
describe('SpanScopeSelector', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render with default ALL_SPANS selected', () => {
renderWithContext();
expect(screen.getByText('All Spans')).toBeInTheDocument();
});
describe('when selecting different options', () => {
const selectOption = (optionText: string): void => {
const selector = screen.getByRole('combobox');
fireEvent.mouseDown(selector);
const option = screen.getByText(optionText);
fireEvent.click(option);
};
const assertFilterAdded = (
updatedQuery: Query,
expectedKey: string,
): void => {
const filters = updatedQuery.builder.queryData[0].filters.items;
expect(filters).toContainEqual(
expect.objectContaining({
key: expect.objectContaining({
key: expectedKey,
type: 'spanSearchScope',
}),
op: '=',
value: 'true',
}),
);
};
it('should remove span scope filters when selecting ALL_SPANS', () => {
const queryWithSpanScope = createQueryWithFilters([
createSpanScopeFilter('isRoot'),
]);
renderWithContext('A', queryWithSpanScope);
selectOption('All Spans');
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
const updatedQuery = mockRedirectWithQueryBuilderData.mock.calls[0][0];
const filters = updatedQuery.builder.queryData[0].filters.items;
expect(filters).not.toContainEqual(
expect.objectContaining({
key: expect.objectContaining({ type: 'spanSearchScope' }),
}),
);
});
it('should add isRoot filter when selecting ROOT_SPANS', async () => {
renderWithContext();
await selectOption('Root Spans');
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
assertFilterAdded(
mockRedirectWithQueryBuilderData.mock.calls[0][0],
'isRoot',
);
});
it('should add isEntryPoint filter when selecting ENTRYPOINT_SPANS', () => {
renderWithContext();
selectOption('Entrypoint Spans');
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
assertFilterAdded(
mockRedirectWithQueryBuilderData.mock.calls[0][0],
'isEntryPoint',
);
});
});
describe('when initializing with existing filters', () => {
it.each([
['Root Spans', 'isRoot'],
['Entrypoint Spans', 'isEntryPoint'],
])(
'should initialize with %s selected when %s filter exists',
async (expectedText, filterKey) => {
const queryWithFilter = createQueryWithFilters([
createSpanScopeFilter(filterKey),
]);
renderWithContext('A', queryWithFilter);
expect(await screen.findByText(expectedText)).toBeInTheDocument();
},
);
});
});

View File

@@ -1,4 +1,5 @@
import { AttributeValuesMap } from 'components/ClientSideQBSearch/ClientSideQBSearch'; import { AttributeValuesMap } from 'components/ClientSideQBSearch/ClientSideQBSearch';
import { OperatorConfigKeys, OPERATORS_CONFIG } from 'constants/queryBuilder';
import { HAVING_FILTER_REGEXP } from 'constants/regExp'; import { HAVING_FILTER_REGEXP } from 'constants/regExp';
import { IOption } from 'hooks/useResourceAttribute/types'; import { IOption } from 'hooks/useResourceAttribute/types';
import uniqWith from 'lodash-es/unionWith'; import uniqWith from 'lodash-es/unionWith';
@@ -110,3 +111,13 @@ export const transformKeyValuesToAttributeValuesMap = (
}, },
]), ]),
); );
export const filterByOperatorConfig = (
options: IOption[],
key?: OperatorConfigKeys,
): IOption[] => {
if (!key || !OPERATORS_CONFIG[key]) return options;
return options.filter((option) =>
OPERATORS_CONFIG[key].includes(option.label),
);
};

View File

@@ -0,0 +1,17 @@
.resourceAttributesFilter-container-v2 {
margin: 8px;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}

View File

@@ -0,0 +1,60 @@
import './ResourceAttributesFilter.styles.scss';
import { initialQueriesMap, OperatorConfigKeys } from 'constants/queryBuilder';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useCallback } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
function ResourceAttributesFilter(): JSX.Element | null {
const { currentQuery } = useQueryBuilder();
const query = currentQuery?.builder?.queryData[0] || null;
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query,
entityVersion: '',
});
// initialise tab with default query.
useShareBuilderUrl({
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
type: 'resource',
},
queryName: '',
},
],
},
});
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
handleChangeQueryData('filters', value);
},
[handleChangeQueryData],
);
return (
<div className="resourceAttributesFilter-container-v2">
<QueryBuilderSearchV2
query={query}
onChange={handleChangeTagFilters}
operatorConfigKey={OperatorConfigKeys.EXCEPTIONS}
/>
</div>
);
}
export default ResourceAttributesFilter;

View File

@@ -284,16 +284,6 @@ function SideNav(): JSX.Element {
manageLicenseMenuItem, manageLicenseMenuItem,
]; ];
const isApiMonitoringEnabled = featureFlags?.find(
(flag) => flag.name === FeatureKeys.THIRD_PARTY_API,
)?.active;
if (!isApiMonitoringEnabled) {
updatedMenuItems = updatedMenuItems.filter(
(item) => item.key !== ROUTES.API_MONITORING,
);
}
if (isCloudUser || isEnterpriseSelfHostedUser) { if (isCloudUser || isEnterpriseSelfHostedUser) {
const isOnboardingEnabled = const isOnboardingEnabled =
featureFlags?.find((feature) => feature.name === FeatureKeys.ONBOARDING) featureFlags?.find((feature) => feature.name === FeatureKeys.ONBOARDING)

View File

@@ -230,6 +230,7 @@ export const routesToSkip = [
ROUTES.CHANNELS_NEW, ROUTES.CHANNELS_NEW,
ROUTES.CHANNELS_EDIT, ROUTES.CHANNELS_EDIT,
ROUTES.WORKSPACE_ACCESS_RESTRICTED, ROUTES.WORKSPACE_ACCESS_RESTRICTED,
ROUTES.ALL_ERROR,
]; ];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS]; export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@@ -0,0 +1,39 @@
.span-line-action-buttons {
display: flex;
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
right: 0;
cursor: pointer;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
.ant-btn-default {
border: none;
box-shadow: none;
padding: 9px;
justify-content: center;
align-items: center;
display: flex;
&.active-tab {
background-color: var(--bg-slate-400);
}
}
.copy-span-btn {
border-color: var(--bg-slate-400) !important;
}
}
.lightMode {
.span-line-action-buttons {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-400);
.copy-span-btn {
border-color: var(--bg-vanilla-400) !important;
}
}
}

View File

@@ -0,0 +1,134 @@
import { fireEvent, screen } from '@testing-library/react';
import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
import { render } from 'tests/test-utils';
import { Span } from 'types/api/trace/getTraceV2';
import SpanLineActionButtons from '../index';
// Mock the useCopySpanLink hook
jest.mock('hooks/trace/useCopySpanLink');
const mockSpan: Span = {
spanId: 'test-span-id',
name: 'test-span',
serviceName: 'test-service',
durationNano: 1000,
timestamp: 1234567890,
rootSpanId: 'test-root-span-id',
parentSpanId: 'test-parent-span-id',
traceId: 'test-trace-id',
hasError: false,
kind: 0,
references: [],
tagMap: {},
event: [],
rootName: 'test-root-name',
statusMessage: 'test-status-message',
statusCodeString: 'test-status-code-string',
spanKind: 'test-span-kind',
hasChildren: false,
hasSibling: false,
subTreeNodeCount: 0,
level: 0,
};
describe('SpanLineActionButtons', () => {
beforeEach(() => {
// Clear mock before each test
jest.clearAllMocks();
});
it('renders copy link button with correct icon', () => {
(useCopySpanLink as jest.Mock).mockReturnValue({
onSpanCopy: jest.fn(),
});
render(<SpanLineActionButtons span={mockSpan} />);
// Check if the button is rendered
const copyButton = screen.getByRole('button');
expect(copyButton).toBeInTheDocument();
// Check if the link icon is rendered
const linkIcon = screen.getByRole('img', { hidden: true });
expect(linkIcon).toHaveClass('anticon anticon-link');
});
it('calls onSpanCopy when copy button is clicked', () => {
const mockOnSpanCopy = jest.fn();
(useCopySpanLink as jest.Mock).mockReturnValue({
onSpanCopy: mockOnSpanCopy,
});
render(<SpanLineActionButtons span={mockSpan} />);
// Click the copy button
const copyButton = screen.getByRole('button');
fireEvent.click(copyButton);
// Verify the copy function was called
expect(mockOnSpanCopy).toHaveBeenCalledTimes(1);
});
it('applies correct styling classes', () => {
(useCopySpanLink as jest.Mock).mockReturnValue({
onSpanCopy: jest.fn(),
});
render(<SpanLineActionButtons span={mockSpan} />);
// Check if the main container has the correct class
const container = screen
.getByRole('button')
.closest('.span-line-action-buttons');
expect(container).toHaveClass('span-line-action-buttons');
// Check if the button has the correct class
const copyButton = screen.getByRole('button');
expect(copyButton).toHaveClass('copy-span-btn');
});
it('copies span link to clipboard when copy button is clicked', () => {
const mockSetCopy = jest.fn();
const mockUrlQuery = {
delete: jest.fn(),
set: jest.fn(),
toString: jest.fn().mockReturnValue('spanId=test-span-id'),
};
const mockPathname = '/test-path';
const mockLocation = {
origin: 'http://localhost:3000',
};
// Mock window.location
Object.defineProperty(window, 'location', {
value: mockLocation,
writable: true,
});
// Mock useCopySpanLink hook
(useCopySpanLink as jest.Mock).mockReturnValue({
onSpanCopy: (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
mockUrlQuery.delete('spanId');
mockUrlQuery.set('spanId', mockSpan.spanId);
const link = `${
window.location.origin
}${mockPathname}?${mockUrlQuery.toString()}`;
mockSetCopy(link);
},
});
render(<SpanLineActionButtons span={mockSpan} />);
// Click the copy button
const copyButton = screen.getByRole('button');
fireEvent.click(copyButton);
// Verify the copy function was called with correct link
expect(mockSetCopy).toHaveBeenCalledWith(
'http://localhost:3000/test-path?spanId=test-span-id',
);
});
});

View File

@@ -0,0 +1,28 @@
import './SpanLineActionButtons.styles.scss';
import { LinkOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
import { Span } from 'types/api/trace/getTraceV2';
export interface SpanLineActionButtonsProps {
span: Span;
}
export default function SpanLineActionButtons({
span,
}: SpanLineActionButtonsProps): JSX.Element {
const { onSpanCopy } = useCopySpanLink(span);
return (
<div className="span-line-action-buttons">
<Tooltip title="Copy Span Link">
<Button
size="small"
icon={<LinkOutlined size={14} />}
onClick={onSpanCopy}
className="copy-span-btn"
/>
</Tooltip>
</div>
);
}

View File

@@ -9,7 +9,10 @@ import cx from 'classnames';
import { TableV3 } from 'components/TableV3/TableV3'; import { TableV3 } from 'components/TableV3/TableV3';
import { themeColors } from 'constants/theme'; import { themeColors } from 'constants/theme';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import SpanLineActionButtons from 'container/TraceWaterfall/SpanLineActionButtons';
import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall'; import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { generateColor } from 'lib/uPlotLib/utils/generateColor'; import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { import {
AlertCircle, AlertCircle,
@@ -25,6 +28,7 @@ import {
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState,
} from 'react'; } from 'react';
import { Span } from 'types/api/trace/getTraceV2'; import { Span } from 'types/api/trace/getTraceV2';
import { toFixed } from 'utils/toFixed'; import { toFixed } from 'utils/toFixed';
@@ -147,7 +151,7 @@ function SpanOverview({
); );
} }
function SpanDuration({ export function SpanDuration({
span, span,
traceMetadata, traceMetadata,
setSelectedSpan, setSelectedSpan,
@@ -166,20 +170,40 @@ function SpanDuration({
const leftOffset = ((span.timestamp - traceMetadata.startTime) * 1e2) / spread; const leftOffset = ((span.timestamp - traceMetadata.startTime) * 1e2) / spread;
const width = (span.durationNano * 1e2) / (spread * 1e6); const width = (span.durationNano * 1e2) / (spread * 1e6);
const urlQuery = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
let color = generateColor(span.serviceName, themeColors.traceDetailColors); let color = generateColor(span.serviceName, themeColors.traceDetailColors);
if (span.hasError) { if (span.hasError) {
color = `var(--bg-cherry-500)`; color = `var(--bg-cherry-500)`;
} }
const [hasActionButtons, setHasActionButtons] = useState(false);
const handleMouseEnter = (): void => {
setHasActionButtons(true);
};
const handleMouseLeave = (): void => {
setHasActionButtons(false);
};
return ( return (
<div <div
className={cx( className={cx(
'span-duration', 'span-duration',
selectedSpan?.spanId === span.spanId ? 'interested-span' : '', selectedSpan?.spanId === span.spanId ? 'interested-span' : '',
)} )}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={(): void => { onClick={(): void => {
setSelectedSpan(span); setSelectedSpan(span);
if (span?.spanId) {
urlQuery.set('spanId', span?.spanId);
}
safeNavigate({ search: urlQuery.toString() });
}} }}
> >
<div <div
@@ -190,6 +214,7 @@ function SpanDuration({
backgroundColor: color, backgroundColor: color,
}} }}
/> />
{hasActionButtons && <SpanLineActionButtons span={span} />}
<Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}> <Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}>
<Typography.Text <Typography.Text
className="span-line-text" className="span-line-text"

View File

@@ -0,0 +1,131 @@
import { fireEvent, screen } from '@testing-library/react';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { render } from 'tests/test-utils';
import { Span } from 'types/api/trace/getTraceV2';
import { SpanDuration } from '../Success';
// Mock the hooks
jest.mock('hooks/useSafeNavigate');
jest.mock('hooks/useUrlQuery');
const mockSpan: Span = {
spanId: 'test-span-id',
name: 'test-span',
serviceName: 'test-service',
durationNano: 1160000, // 1ms in nano
timestamp: 1234567890,
rootSpanId: 'test-root-span-id',
parentSpanId: 'test-parent-span-id',
traceId: 'test-trace-id',
hasError: false,
kind: 0,
references: [],
tagMap: {},
event: [],
rootName: 'test-root-name',
statusMessage: 'test-status-message',
statusCodeString: 'test-status-code-string',
spanKind: 'test-span-kind',
hasChildren: false,
hasSibling: false,
subTreeNodeCount: 0,
level: 0,
};
const mockTraceMetadata = {
traceId: 'test-trace-id',
startTime: 1234567000,
endTime: 1234569000,
hasMissingSpans: false,
};
describe('SpanDuration', () => {
const mockSetSelectedSpan = jest.fn();
const mockUrlQuerySet = jest.fn();
const mockSafeNavigate = jest.fn();
const mockUrlQueryGet = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
// Mock URL query hook
(useUrlQuery as jest.Mock).mockReturnValue({
set: mockUrlQuerySet,
get: mockUrlQueryGet,
toString: () => 'spanId=test-span-id',
});
// Mock safe navigate hook
(useSafeNavigate as jest.Mock).mockReturnValue({
safeNavigate: mockSafeNavigate,
});
});
it('updates URL and selected span when clicked', () => {
render(
<SpanDuration
span={mockSpan}
traceMetadata={mockTraceMetadata}
selectedSpan={undefined}
setSelectedSpan={mockSetSelectedSpan}
/>,
);
// Find and click the span duration element
const spanElement = screen.getByText('1.16 ms');
fireEvent.click(spanElement);
// Verify setSelectedSpan was called with the correct span
expect(mockSetSelectedSpan).toHaveBeenCalledWith(mockSpan);
// Verify URL query was updated
expect(mockUrlQuerySet).toHaveBeenCalledWith('spanId', 'test-span-id');
// Verify navigation was triggered
expect(mockSafeNavigate).toHaveBeenCalledWith({
search: 'spanId=test-span-id',
});
});
it('shows action buttons on hover', () => {
render(
<SpanDuration
span={mockSpan}
traceMetadata={mockTraceMetadata}
selectedSpan={undefined}
setSelectedSpan={mockSetSelectedSpan}
/>,
);
const spanElement = screen.getByText('1.16 ms');
// Initially, action buttons should not be visible
expect(screen.queryByRole('button')).not.toBeInTheDocument();
// Hover over the span
fireEvent.mouseEnter(spanElement);
// Action buttons should now be visible
expect(screen.getByRole('button')).toBeInTheDocument();
// Mouse leave should hide the buttons
fireEvent.mouseLeave(spanElement);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
it('applies interested-span class when span is selected', () => {
render(
<SpanDuration
span={mockSpan}
traceMetadata={mockTraceMetadata}
selectedSpan={mockSpan}
setSelectedSpan={mockSetSelectedSpan}
/>,
);
const spanElement = screen.getByText('1.16 ms').closest('.span-duration');
expect(spanElement).toHaveClass('interested-span');
});
});

View File

@@ -170,11 +170,7 @@ export const useOptions = (
(option, index, self) => (option, index, self) =>
index === index ===
self.findIndex( self.findIndex(
(o) => (o) => o.label === option.label && o.value === option.value, // to remove duplicate & empty options from list
// to remove duplicate & empty options from list
o.label === option.label &&
o.value === option.value &&
o.dataType?.toLowerCase() === option.dataType?.toLowerCase(), // handle case sensitivity
) && option.value !== '', ) && option.value !== '',
) || [] ) || []
).map((option) => { ).map((option) => {

View File

@@ -0,0 +1,42 @@
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { MouseEventHandler, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Span } from 'types/api/trace/getTraceV2';
export const useCopySpanLink = (
span?: Span,
): { onSpanCopy: MouseEventHandler<HTMLElement> } => {
const urlQuery = useUrlQuery();
const { pathname } = useLocation();
const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
const onSpanCopy: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
if (!span) return;
event.preventDefault();
event.stopPropagation();
urlQuery.delete('spanId');
if (span.spanId) {
urlQuery.set('spanId', span?.spanId);
}
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
setCopy(link);
notifications.success({
message: 'Copied to clipboard',
});
},
[span, urlQuery, pathname, setCopy, notifications],
);
return {
onSpanCopy,
};
};

View File

@@ -2,7 +2,10 @@ import {
getResourceAttributesTagKeys, getResourceAttributesTagKeys,
getResourceAttributesTagValues, getResourceAttributesTagValues,
} from 'api/metrics/getResourceAttributes'; } from 'api/metrics/getResourceAttributes';
import { OperatorConversions } from 'constants/resourceAttributes'; import {
CompositeQueryOperatorsConfig,
OperatorConversions,
} from 'constants/resourceAttributes';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { MetricsType } from 'container/MetricsApplication/constant'; import { MetricsType } from 'container/MetricsApplication/constant';
import { import {
@@ -49,6 +52,32 @@ export const convertOperatorLabelToTraceOperator = (
OperatorConversions.find((operator) => operator.label === label) OperatorConversions.find((operator) => operator.label === label)
?.traceValue as OperatorValues; ?.traceValue as OperatorValues;
export function convertOperatorLabelForExceptions(
label: string,
): OperatorValues {
return CompositeQueryOperatorsConfig.find(
(operator) => operator.label === label,
)?.traceValue as OperatorValues;
}
export function formatStringValuesForTrace(
val: TagFilterItem['value'] = [],
): string[] {
return !Array.isArray(val) ? [String(val)] : val;
}
export const convertCompositeQueryToTraceSelectedTags = (
filterItems: TagFilterItem[] = [],
): Tags[] =>
filterItems.map((item) => ({
Key: item?.key?.key,
Operator: convertOperatorLabelForExceptions(item.op),
StringValues: formatStringValuesForTrace(item?.value),
NumberValues: [],
BoolValues: [],
TagType: 'ResourceAttribute',
})) as Tags[];
export const convertRawQueriesToTraceSelectedTags = ( export const convertRawQueriesToTraceSelectedTags = (
queries: IResourceAttribute[], queries: IResourceAttribute[],
tagType = 'ResourceAttribute', tagType = 'ResourceAttribute',

View File

@@ -44,7 +44,7 @@ function AlertActionButtons({
const { handleAlertDuplicate } = useAlertRuleDuplicate({ const { handleAlertDuplicate } = useAlertRuleDuplicate({
alertDetails: (alertDetails as unknown) as AlertDef, alertDetails: (alertDetails as unknown) as AlertDef,
}); });
const { handleAlertDelete } = useAlertRuleDelete({ ruleId: Number(ruleId) }); const { handleAlertDelete } = useAlertRuleDelete({ ruleId });
const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({ const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({
alertDetails: (alertDetails as unknown) as AlertDef, alertDetails: (alertDetails as unknown) as AlertDef,
setUpdatedName, setUpdatedName,

View File

@@ -153,7 +153,7 @@ type Props = {
export const useGetAlertRuleDetails = (): Props => { export const useGetAlertRuleDetails = (): Props => {
const { ruleId } = useAlertHistoryQueryParams(); const { ruleId } = useAlertHistoryQueryParams();
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0; const isValidRuleId = ruleId !== null && ruleId !== '';
const { const {
isLoading, isLoading,
@@ -163,7 +163,7 @@ export const useGetAlertRuleDetails = (): Props => {
} = useQuery([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId], { } = useQuery([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId], {
queryFn: () => queryFn: () =>
get({ get({
id: parseInt(ruleId || '', 10), id: ruleId || '',
}), }),
enabled: isValidRuleId, enabled: isValidRuleId,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
@@ -204,7 +204,7 @@ export const useGetAlertRuleDetailsStats = (): GetAlertRuleDetailsStatsProps =>
{ {
queryFn: () => queryFn: () =>
ruleStats({ ruleStats({
id: parseInt(ruleId || '', 10), id: ruleId || '',
start: startTime, start: startTime,
end: endTime, end: endTime,
}), }),
@@ -234,7 +234,7 @@ export const useGetAlertRuleDetailsTopContributors = (): GetAlertRuleDetailsTopC
{ {
queryFn: () => queryFn: () =>
topContributors({ topContributors({
id: parseInt(ruleId || '', 10), id: ruleId || '',
start: startTime, start: startTime,
end: endTime, end: endTime,
}), }),
@@ -287,7 +287,7 @@ export const useGetAlertRuleDetailsTimelineTable = ({
{ {
queryFn: () => queryFn: () =>
timelineTable({ timelineTable({
id: parseInt(ruleId || '', 10), id: ruleId || '',
start: startTime, start: startTime,
end: endTime, end: endTime,
limit: TIMELINE_TABLE_PAGE_SIZE, limit: TIMELINE_TABLE_PAGE_SIZE,
@@ -410,7 +410,7 @@ export const useAlertRuleStatusToggle = ({
const handleAlertStateToggle = (): void => { const handleAlertStateToggle = (): void => {
const args = { const args = {
id: parseInt(ruleId, 10), id: ruleId,
data: { disabled: alertRuleState !== 'disabled' }, data: { disabled: alertRuleState !== 'disabled' },
}; };
toggleAlertState(args); toggleAlertState(args);
@@ -512,7 +512,7 @@ export const useAlertRuleUpdate = ({
export const useAlertRuleDelete = ({ export const useAlertRuleDelete = ({
ruleId, ruleId,
}: { }: {
ruleId: number; ruleId: string;
}): { }): {
handleAlertDelete: () => void; handleAlertDelete: () => void;
} => { } => {
@@ -560,7 +560,7 @@ export const useGetAlertRuleDetailsTimelineGraphData = (): GetAlertRuleDetailsTi
{ {
queryFn: () => queryFn: () =>
timelineGraph({ timelineGraph({
id: parseInt(ruleId || '', 10), id: ruleId || '',
start: startTime, start: startTime,
end: endTime, end: endTime,
}), }),

View File

@@ -0,0 +1,27 @@
.all-errors-page {
display: flex;
height: 100%;
.all-errors-quick-filter-section {
width: 0%;
flex-shrink: 0;
color: var(--bg-vanilla-100);
}
.all-errors-right-section {
padding: 0 10px;
}
.ant-tabs {
margin: 0 8px;
}
&.filter-visible {
.all-errors-quick-filter-section {
width: 260px;
}
.all-errors-right-section {
width: calc(100% - 260px);
}
}
}

View File

@@ -1,18 +1,87 @@
import './AllErrors.styles.scss';
import { FilterOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import RouteTab from 'components/RouteTab'; import RouteTab from 'components/RouteTab';
import ResourceAttributesFilter from 'container/ResourceAttributesFilter'; import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
import { LOCALSTORAGE } from 'constants/localStorage';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import ResourceAttributesFilterV2 from 'container/ResourceAttributeFilterV2/ResourceAttributesFilterV2';
import Toolbar from 'container/Toolbar/Toolbar';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import history from 'lib/history'; import history from 'lib/history';
import { isNull } from 'lodash-es';
import { useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { routes } from './config'; import { routes } from './config';
import { ExceptionsQuickFiltersConfig } from './utils';
function AllErrors(): JSX.Element { function AllErrors(): JSX.Element {
const { pathname } = useLocation(); const { pathname } = useLocation();
const { handleRunQuery } = useQueryBuilder();
const [showFilters, setShowFilters] = useState<boolean>(() => {
const localStorageValue = getLocalStorageKey(
LOCALSTORAGE.SHOW_EXCEPTIONS_QUICK_FILTERS,
);
if (!isNull(localStorageValue)) {
return localStorageValue === 'true';
}
return true;
});
const handleFilterVisibilityChange = (): void => {
setLocalStorageApi(
LOCALSTORAGE.SHOW_EXCEPTIONS_QUICK_FILTERS,
String(!showFilters),
);
setShowFilters((prev) => !prev);
};
return ( return (
<> <div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
<ResourceAttributesFilter /> {showFilters && (
<RouteTab routes={routes} activeKey={pathname} history={history} /> <section className={cx('all-errors-quick-filter-section')}>
</> <QuickFilters
source={QuickFiltersSource.EXCEPTIONS}
config={ExceptionsQuickFiltersConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
/>
</section>
)}
<section
className={cx(
'all-errors-right-section',
showFilters ? 'filter-visible' : '',
)}
>
<TypicalOverlayScrollbar>
<>
<Toolbar
showAutoRefresh={false}
leftActions={
!showFilters ? (
<Tooltip title="Show Filters">
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
<FilterOutlined />
</Button>
</Tooltip>
) : undefined
}
rightActions={<RightToolbarActions onStageRunQuery={handleRunQuery} />}
/>
<ResourceAttributesFilterV2 />
<RouteTab routes={routes} activeKey={pathname} history={history} />
</>
</TypicalOverlayScrollbar>
</section>
</div>
); );
} }

View File

@@ -0,0 +1,100 @@
import {
FiltersType,
IQuickFiltersConfig,
} from 'components/QuickFilters/types';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
export const ExceptionsQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'Environment',
dataSource: DataSource.TRACES,
attributeKey: {
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'Service Name',
dataSource: DataSource.TRACES,
attributeKey: {
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: false,
},
{
type: FiltersType.CHECKBOX,
title: 'Hostname',
dataSource: DataSource.TRACES,
attributeKey: {
key: 'host.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: false,
},
{
type: FiltersType.CHECKBOX,
title: 'K8s Cluster Name',
dataSource: DataSource.TRACES,
attributeKey: {
key: 'k8s.cluster.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: false,
},
{
type: FiltersType.CHECKBOX,
title: 'K8s Deployment Name',
dataSource: DataSource.TRACES,
attributeKey: {
key: 'k8s.deployment.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: false,
},
{
type: FiltersType.CHECKBOX,
title: 'K8s Namespace Name',
dataSource: DataSource.TRACES,
attributeKey: {
key: 'k8s.namespace.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: false,
},
{
type: FiltersType.CHECKBOX,
title: 'K8s Pod Name',
dataSource: DataSource.TRACES,
attributeKey: {
key: 'k8s.pod.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: false,
},
];

View File

@@ -34,7 +34,7 @@ function EditRules(): JSX.Element {
{ {
queryFn: () => queryFn: () =>
get({ get({
id: parseInt(ruleId || '', 10), id: ruleId || '',
}), }),
enabled: isValidRuleId, enabled: isValidRuleId,
refetchOnMount: false, refetchOnMount: false,
@@ -90,10 +90,7 @@ function EditRules(): JSX.Element {
return ( return (
<div className="edit-rules-container"> <div className="edit-rules-container">
<EditRulesContainer <EditRulesContainer ruleId={ruleId || ''} initialValue={data.payload.data} />
ruleId={parseInt(ruleId, 10)}
initialValue={data.payload.data}
/>
</div> </div>
); );
} }

View File

@@ -143,7 +143,7 @@ export default function Support(): JSX.Element {
}); });
updateCreditCard({ updateCreditCard({
url: window.location.href, url: window.location.origin,
}); });
}; };

View File

@@ -109,7 +109,7 @@ export default function WorkspaceBlocked(): JSX.Element {
logEvent('Workspace Blocked: User Clicked Update Credit Card', {}); logEvent('Workspace Blocked: User Clicked Update Credit Card', {});
updateCreditCard({ updateCreditCard({
url: window.location.href, url: window.location.origin,
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [updateCreditCard]); }, [updateCreditCard]);

View File

@@ -1,6 +1,7 @@
import './LineClampedText.styles.scss'; import './LineClampedText.styles.scss';
import { Tooltip, TooltipProps } from 'antd'; import { Tooltip, TooltipProps } from 'antd';
import { isBoolean } from 'lodash-es';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
function LineClampedText({ function LineClampedText({
@@ -8,7 +9,7 @@ function LineClampedText({
lines, lines,
tooltipProps, tooltipProps,
}: { }: {
text: string; text: string | boolean;
lines?: number; lines?: number;
tooltipProps?: TooltipProps; tooltipProps?: TooltipProps;
}): JSX.Element { }): JSX.Element {
@@ -40,7 +41,7 @@ function LineClampedText({
WebkitLineClamp: lines, WebkitLineClamp: lines,
}} }}
> >
{text} {isBoolean(text) ? String(text) : text}
</div> </div>
); );

View File

@@ -0,0 +1,64 @@
import { render, screen } from '@testing-library/react';
import LineClampedText from '../LineClampedText';
describe('LineClampedText', () => {
// Reset all mocks after each test
afterEach(() => {
jest.clearAllMocks();
});
it('renders string text correctly', () => {
const text = 'Test text';
render(<LineClampedText text={text} />);
expect(screen.getByText(text)).toBeInTheDocument();
});
it('renders empty string correctly', () => {
const { container } = render(<LineClampedText text="" />);
// For empty strings, we need to check that a div exists
// but it's harder to check for empty text directly with queries
expect(container.textContent).toBe('');
});
it('renders boolean text correctly - true', () => {
render(<LineClampedText text />);
expect(screen.getByText('true')).toBeInTheDocument();
});
it('renders boolean text correctly - false', () => {
render(<LineClampedText text={false} />);
expect(screen.getByText('false')).toBeInTheDocument();
});
it('applies line clamping with provided lines prop', () => {
const text = 'Test text with multiple lines';
const lines = 2;
render(<LineClampedText text={text} lines={lines} />);
// Verify the text is rendered correctly
expect(screen.getByText(text)).toBeInTheDocument();
// Verify the component received the correct props
expect((screen.getByText(text).style as any).WebkitLineClamp).toBe(
String(lines),
);
});
it('uses default line count of 1 when lines prop is not provided', () => {
const text = 'Test text';
render(<LineClampedText text={text} />);
// Verify the text is rendered correctly
expect(screen.getByText(text)).toBeInTheDocument();
// Verify the default props
expect(LineClampedText.defaultProps?.lines).toBe(1);
});
});

View File

@@ -17,7 +17,7 @@ export const defaultAlgorithm = 'standard';
export const defaultSeasonality = 'hourly'; export const defaultSeasonality = 'hourly';
export interface AlertDef { export interface AlertDef {
id?: number; id?: string;
alertType?: string; alertType?: string;
alert: string; alert: string;
ruleType?: string; ruleType?: string;

View File

@@ -5,7 +5,7 @@ export interface Props {
} }
export interface GettableAlert extends AlertDef { export interface GettableAlert extends AlertDef {
id: number; id: string;
alert: string; alert: string;
state: string; state: string;
disabled: boolean; disabled: boolean;

View File

@@ -7,6 +7,6 @@ export interface PatchProps {
} }
export interface Props { export interface Props {
id?: number; id?: string;
data: PatchProps; data: PatchProps;
} }

View File

@@ -6,6 +6,6 @@ export type PayloadProps = {
}; };
export interface Props { export interface Props {
id?: number; id?: string;
data: AlertDef; data: AlertDef;
} }

View File

@@ -6852,18 +6852,17 @@ copy-to-clipboard@^3.3.1, copy-to-clipboard@^3.3.3:
dependencies: dependencies:
toggle-selection "^1.0.6" toggle-selection "^1.0.6"
copy-webpack-plugin@^8.1.0: copy-webpack-plugin@^11.0.0:
version "8.1.1" version "11.0.0"
resolved "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-8.1.1.tgz" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a"
integrity sha512-rYM2uzRxrLRpcyPqGceRBDpxxUV8vcDqIKxAUKfcnFpcrPxT5+XvhTxv7XLjo5AvEJFPdAE3zCogG2JVahqgSQ== integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==
dependencies: dependencies:
fast-glob "^3.2.5" fast-glob "^3.2.11"
glob-parent "^5.1.1" glob-parent "^6.0.1"
globby "^11.0.3" globby "^13.1.1"
normalize-path "^3.0.0" normalize-path "^3.0.0"
p-limit "^3.1.0" schema-utils "^4.0.0"
schema-utils "^3.0.0" serialize-javascript "^6.0.0"
serialize-javascript "^5.0.1"
core-js-compat@^3.25.1: core-js-compat@^3.25.1:
version "3.30.1" version "3.30.1"
@@ -8740,7 +8739,7 @@ fast-diff@^1.1.2:
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-glob@^3.0.3: fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.3.0:
version "3.3.3" version "3.3.3"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
@@ -8751,7 +8750,7 @@ fast-glob@^3.0.3:
merge2 "^1.3.0" merge2 "^1.3.0"
micromatch "^4.0.8" micromatch "^4.0.8"
fast-glob@^3.2.5, fast-glob@^3.2.9: fast-glob@^3.2.9:
version "3.2.12" version "3.2.12"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz"
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
@@ -9307,13 +9306,20 @@ gl-preserve-state@^1.0.0:
resolved "https://registry.npmjs.org/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz" resolved "https://registry.npmjs.org/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz"
integrity sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q== integrity sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q==
glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2: glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies: dependencies:
is-glob "^4.0.1" is-glob "^4.0.1"
glob-parent@^6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
dependencies:
is-glob "^4.0.3"
glob-to-regexp@^0.4.1: glob-to-regexp@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz"
@@ -9401,6 +9407,17 @@ globby@^11.0.3, globby@^11.1.0:
merge2 "^1.4.1" merge2 "^1.4.1"
slash "^3.0.0" slash "^3.0.0"
globby@^13.1.1:
version "13.2.2"
resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592"
integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==
dependencies:
dir-glob "^3.0.1"
fast-glob "^3.3.0"
ignore "^5.2.4"
merge2 "^1.4.1"
slash "^4.0.0"
gopd@^1.0.1: gopd@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz"
@@ -10061,6 +10078,11 @@ ignore@^5.1.1, ignore@^5.1.8, ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
ignore@^5.2.4:
version "5.3.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
image-size@~0.5.0: image-size@~0.5.0:
version "0.5.5" version "0.5.5"
resolved "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz" resolved "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz"
@@ -13401,7 +13423,7 @@ p-limit@^2.2.0:
dependencies: dependencies:
p-try "^2.0.0" p-try "^2.0.0"
p-limit@^3.0.2, p-limit@^3.1.0: p-limit@^3.0.2:
version "3.1.0" version "3.1.0"
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
@@ -15845,17 +15867,10 @@ send@0.19.0:
range-parser "~1.2.1" range-parser "~1.2.1"
statuses "2.0.1" statuses "2.0.1"
serialize-javascript@^5.0.1: serialize-javascript@6.0.2, serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
version "5.0.1" version "6.0.2"
resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
dependencies:
randombytes "^2.1.0"
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz"
integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==
dependencies: dependencies:
randombytes "^2.1.0" randombytes "^2.1.0"
@@ -16012,6 +16027,11 @@ slash@^3.0.0:
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
slash@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
slice-ansi@^3.0.0: slice-ansi@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz" resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz"

6
go.mod
View File

@@ -10,7 +10,8 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 github.com/ClickHouse/clickhouse-go/v2 v2.30.0
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.111.16 github.com/SigNoz/signoz-otel-collector v0.111.39
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3 github.com/antonmedv/expr v1.15.3
github.com/cespare/xxhash/v2 v2.3.0 github.com/cespare/xxhash/v2 v2.3.0
github.com/coreos/go-oidc/v3 v3.11.0 github.com/coreos/go-oidc/v3 v3.11.0
@@ -89,10 +90,9 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/ClickHouse/ch-go v0.61.5 // indirect github.com/ClickHouse/ch-go v0.63.1 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/armon/go-metrics v0.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect

14
go.sum
View File

@@ -85,8 +85,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4= github.com/ClickHouse/ch-go v0.63.1 h1:s2JyZvWLTCSAGdtjMBBmAgQQHMco6pawLJMOXi0FODM=
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= github.com/ClickHouse/ch-go v0.63.1/go.mod h1:I1kJJCL3WJcBMGe1m+HVK0+nREaG+JOYYBWjrDrF3R0=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo= github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo= github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo=
github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU=
@@ -100,8 +100,10 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8=
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
github.com/SigNoz/signoz-otel-collector v0.111.16 h1:535uKH5Oux+35EsI+L3C6pnAP/Ye0PTCbVizXoL+VqE= github.com/SigNoz/signoz-otel-collector v0.111.39-beta.1 h1:ZpSNrOZBOH2iCJIPeER5X0mfxOe64yP3JRX7FzBNfwY=
github.com/SigNoz/signoz-otel-collector v0.111.16/go.mod h1:HJ4m0LY1MPsuZmuRF7Ixb+bY8rxgRzI0VXzOedESsjg= github.com/SigNoz/signoz-otel-collector v0.111.39-beta.1/go.mod h1:DCu/D+lqhsPNSGS4IMD+4gn7q06TGzOCKazSy+GURVc=
github.com/SigNoz/signoz-otel-collector v0.111.39 h1:Dl8QqZNAsj2atxP572OzsszPK0XPpd3LLPNPRAUJ5wo=
github.com/SigNoz/signoz-otel-collector v0.111.39/go.mod h1:DCu/D+lqhsPNSGS4IMD+4gn7q06TGzOCKazSy+GURVc=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -820,8 +822,8 @@ github.com/prometheus/prometheus v0.300.1/go.mod h1:gtTPY/XVyCdqqnjA3NzDMb0/nc5H
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= github.com/redis/go-redis/v9 v9.6.3 h1:8Dr5ygF1QFXRxIH/m3Xg9MMG1rS8YCtAgosrsewT6i0=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/redis/go-redis/v9 v9.6.3/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=

View File

@@ -104,7 +104,7 @@ fullText
* ... * ...
*/ */
functionCall functionCall
: (HAS | HASANY | HASALL | HASNONE) LPAREN functionParamList RPAREN : (HAS | HASANY | HASALL) LPAREN functionParamList RPAREN
; ;
// Function parameters can be keys, single scalar values, or arrays // Function parameters can be keys, single scalar values, or arrays
@@ -182,7 +182,6 @@ OR : [Oo][Rr] ;
HAS : [Hh][Aa][Ss] ; HAS : [Hh][Aa][Ss] ;
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ; HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ; HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
HASNONE : [Hh][Aa][Ss][Nn][Oo][Nn][Ee] ;
// Potential boolean constants // Potential boolean constants
BOOL BOOL
@@ -205,7 +204,7 @@ QUOTED_TEXT
// Keys can have letters, digits, underscores, dots, and bracket pairs // Keys can have letters, digits, underscores, dots, and bracket pairs
// e.g. service.name, service.namespace, db.queries[].query_duration // e.g. service.name, service.namespace, db.queries[].query_duration
KEY KEY
: [a-zA-Z0-9_] [a-zA-Z0-9_.[\]]* : [a-zA-Z0-9_] [a-zA-Z0-9_.*[\]]*
; ;
// Ignore whitespace // Ignore whitespace
@@ -218,4 +217,4 @@ fragment DIGIT
: [0-9] : [0-9]
; ;
FREETEXT : (~[ \t\r\n=()'"<>![\]])+ ; FREETEXT : (~[ \t\r\n=()'"<>!,[\]])+ ;

View File

@@ -3,7 +3,6 @@ package sqlalertmanagerstore
import ( import (
"context" "context"
"database/sql" "database/sql"
"strconv"
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
@@ -191,9 +190,9 @@ func (store *config) ListAllChannels(ctx context.Context) ([]*alertmanagertypes.
func (store *config) GetMatchers(ctx context.Context, orgID string) (map[string][]string, error) { func (store *config) GetMatchers(ctx context.Context, orgID string) (map[string][]string, error) {
type matcher struct { type matcher struct {
bun.BaseModel `bun:"table:rules"` bun.BaseModel `bun:"table:rule"`
ID int `bun:"id,pk"` ID valuer.UUID `bun:"id,pk"`
Data string `bun:"data"` Data string `bun:"data"`
} }
matchers := []matcher{} matchers := []matcher{}
@@ -213,7 +212,7 @@ func (store *config) GetMatchers(ctx context.Context, orgID string) (map[string]
for _, matcher := range matchers { for _, matcher := range matchers {
receivers := gjson.Get(matcher.Data, "preferredChannels").Array() receivers := gjson.Get(matcher.Data, "preferredChannels").Array()
for _, receiver := range receivers { for _, receiver := range receivers {
matchersMap[strconv.Itoa(matcher.ID)] = append(matchersMap[strconv.Itoa(matcher.ID)], receiver.String()) matchersMap[matcher.ID.StringValue()] = append(matchersMap[matcher.ID.StringValue()], receiver.String())
} }
} }

View File

@@ -25,6 +25,25 @@ type postableAlert struct {
Receivers []string `json:"receivers"` Receivers []string `json:"receivers"`
} }
func (pa *postableAlert) MarshalJSON() ([]byte, error) {
// Marshal the embedded PostableAlert to get its JSON representation.
alertJSON, err := json.Marshal(pa.PostableAlert)
if err != nil {
return nil, err
}
// Unmarshal that JSON into a map so we can add extra fields.
var m map[string]interface{}
if err := json.Unmarshal(alertJSON, &m); err != nil {
return nil, err
}
// Add the Receivers field.
m["receivers"] = pa.Receivers
return json.Marshal(m)
}
const ( const (
alertsPath string = "/v1/alerts" alertsPath string = "/v1/alerts"
routesPath string = "/v1/routes" routesPath string = "/v1/routes"

View File

@@ -0,0 +1,35 @@
package legacyalertmanager
import (
"encoding/json"
"testing"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/stretchr/testify/assert"
)
func TestProvider_TestAlert(t *testing.T) {
pa := &postableAlert{
PostableAlert: &alertmanagertypes.PostableAlert{
Alert: models.Alert{
Labels: models.LabelSet{
"alertname": "test",
},
GeneratorURL: "http://localhost:9090/graph?g0.expr=up&g0.tab=1",
},
Annotations: models.LabelSet{
"summary": "test",
},
},
Receivers: []string{"receiver1", "receiver2"},
}
body, err := json.Marshal(pa)
if err != nil {
t.Fatalf("failed to marshal postable alert: %v", err)
}
assert.Contains(t, string(body), "receiver1")
assert.Contains(t, string(body), "receiver2")
}

Some files were not shown because too many files have changed in this diff Show More