Compare commits

..

22 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
127 changed files with 4630 additions and 1759 deletions

View File

@@ -1,61 +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 PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
export KAFKA_SPAN_EVAL="true"
docker system prune --force --all
OTELCOL_TAG=$(curl -s https://api.github.com/repos/SigNoz/signoz-otel-collector/releases/latest | jq -r '.tag_name // "not-found"')
if [[ "${OTELCOL_TAG}" == "not-found" ]]; then
echo "warning: unable to determine latest SigNoz OtelCollector release tag, skipping latest otelcol deployment"
else
export OTELCOL_TAG=${OTELCOL_TAG}
docker pull signoz/signoz-otel-collector:${OTELCOL_TAG}
docker pull signoz/signoz-schema-migrator:${OTELCOL_TAG}
fi
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 --all
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}"

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.79.1
image: signoz/signoz:v0.80.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.79.1
image: signoz/signoz:v0.80.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.79.1}
image: signoz/signoz:${VERSION:-v0.80.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

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.79.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.39}
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.39}
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.39}
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
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.79.1}
image: signoz/signoz:${VERSION:-v0.80.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

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

View File

@@ -367,6 +367,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterCloudIntegrationsRoutes(r, am)
apiHandler.RegisterFieldsRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
@@ -428,11 +429,11 @@ func (s *Server) initListeners() error {
}
// 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
if !s.serverOptions.DisableRules {
s.ruleManager.Start()
s.ruleManager.Start(ctx)
} else {
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()
if s.ruleManager != nil {
s.ruleManager.Stop()
s.ruleManager.Stop(context.Background())
}
// stop usage manager

View File

@@ -143,7 +143,7 @@ func main() {
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))
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/cache"
"github.com/SigNoz/signoz/pkg/query-service/common"
"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"
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
@@ -52,7 +53,7 @@ type AnomalyRule struct {
func NewAnomalyRule(
id string,
p *baserules.PostableRule,
p *ruletypes.PostableRule,
reader interfaces.Reader,
cache cache.Cache,
opts ...baserules.RuleOption,
@@ -60,7 +61,7 @@ func NewAnomalyRule(
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
p.RuleCondition.Target = &target
}
@@ -117,7 +118,7 @@ func NewAnomalyRule(
return &t, nil
}
func (r *AnomalyRule) Type() baserules.RuleType {
func (r *AnomalyRule) Type() ruletypes.RuleType {
return RuleTypeAnomaly
}
@@ -157,7 +158,7 @@ func (r *AnomalyRule) GetSelectedQuery() string {
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)
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)
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()
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 {
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())
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
// who are not used to Go's templating system.
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
expand := func(text string) string {
tmpl := baserules.NewTemplateExpander(
tmpl := ruletypes.NewTemplateExpander(
ctx,
defs+text,
"__alert_"+r.Name(),
@@ -278,7 +279,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
return nil, err
}
alerts[h] = &baserules.Alert{
alerts[h] = &ruletypes.Alert{
Labels: lbs,
QueryResultLables: resultLabels,
Annotations: annotations,
@@ -319,7 +320,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
if _, ok := resultFPs[fp]; !ok {
// If the alert was previously firing, keep it around for a given
// 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)
}
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 {
ar := baserules.PostableRule{
ar := ruletypes.PostableRule{
AlertName: r.Name(),
RuleCondition: r.Condition(),
EvalWindow: baserules.Duration(r.EvalWindow()),
EvalWindow: ruletypes.Duration(r.EvalWindow()),
Labels: r.Labels().Map(),
Annotations: r.Annotations().Map(),
PreferredChannels: r.PreferredChannels(),

View File

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

@@ -198,7 +198,7 @@
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.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",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^19.0.4",
@@ -255,6 +255,7 @@
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.3",
"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 { PayloadProps, Props } from 'types/api/alerts/save';
@@ -7,7 +8,7 @@ import put from './put';
const save = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
if (props.id && props.id > 0) {
if (props.id && !isEmpty(props.id)) {
return put({ ...props });
}

View File

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

View File

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

View File

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

View File

@@ -27,4 +27,5 @@ export enum LOCALSTORAGE {
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING',
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[] = [
OPERATORS['='],
OPERATORS['!='],

View File

@@ -16,3 +16,51 @@ export const OperatorConversions: Array<{
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 { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useNotifications } from 'hooks/useNotifications';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { convertCompositeQueryToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
@@ -109,10 +110,11 @@ function AllErrors(): JSX.Element {
);
const { queries } = useResourceAttribute();
const compositeData = useGetCompositeQueryParam();
const [{ isLoading, data }, errorCountResponse] = useQueries([
{
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries],
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, compositeData],
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
getAll({
end: maxTime,
@@ -123,7 +125,9 @@ function AllErrors(): JSX.Element {
orderParam: getUpdatedParams,
exceptionType: getUpdatedExceptionType,
serviceName: getUpdatedServiceName,
tags: convertRawQueriesToTraceSelectedTags(queries),
tags: convertCompositeQueryToTraceSelectedTags(
compositeData?.builder.queryData?.[0]?.filters.items,
),
}),
enabled: !loading,
},
@@ -134,7 +138,7 @@ function AllErrors(): JSX.Element {
minTime,
getUpdatedExceptionType,
getUpdatedServiceName,
queries,
compositeData,
],
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
getErrorCounts({
@@ -142,7 +146,9 @@ function AllErrors(): JSX.Element {
start: minTime,
exceptionType: getUpdatedExceptionType,
serviceName: getUpdatedServiceName,
tags: convertRawQueriesToTraceSelectedTags(queries),
tags: convertCompositeQueryToTraceSelectedTags(
compositeData?.builder.queryData?.[0]?.filters.items,
),
}),
enabled: !loading,
},
@@ -429,12 +435,8 @@ function AllErrors(): JSX.Element {
[pathname],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (
!logEventCalledRef.current &&
!isUndefined(errorCountResponse.data?.payload)
) {
if (!isUndefined(errorCountResponse.data?.payload)) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
@@ -442,9 +444,12 @@ function AllErrors(): JSX.Element {
logEvent('Exception: List page visited', {
numberOfExceptions: errorCountResponse?.data?.payload,
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
}, [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 isExceptionsView = (): boolean => routeKey === 'ALL_ERROR';
const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
@@ -661,7 +663,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring() ||
isApiMonitoringView()
isApiMonitoringView() ||
isExceptionsView()
? 0
: '0 1rem',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import './Home.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover } from 'antd';
import { Alert, Button, Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
@@ -644,6 +644,16 @@ export default function Home(): JSX.Element {
</div>
<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 && (
<AnimatePresence initial={false}>
<Card className="checklist-card">

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import dompurify from 'dompurify';
import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
@@ -142,7 +143,7 @@ export function TableViewActions(
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
{renderFieldContent()}
</CopyClipboardHOC>
{!isListViewPanel && (
{!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
<span className="action-btn">
<Tooltip title="Filter for value">
<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 (
props: DowntimeScheduleUpdatePayload,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
if (props.id && props.id > 0) {
if (props.id) {
return updateDowntimeSchedule({ ...props });
}
return createDowntimeSchedule({ ...props.data });

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,11 @@
.rc-virtual-list-holder {
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 {
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
OperatorConfigKeys,
OPERATORS,
QUERY_BUILDER_OPERATORS_BY_TYPES,
QUERY_BUILDER_SEARCH_VALUES,
@@ -62,7 +63,9 @@ import {
getTagToken,
isInNInOperator,
} from '../QueryBuilderSearch/utils';
import { filterByOperatorConfig } from '../utils';
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
import SpanScopeSelector from './SpanScopeSelector';
import Suggestions from './Suggestions';
export interface ITag {
@@ -88,6 +91,7 @@ interface QueryBuilderSearchV2Props {
className?: string;
suffixIcon?: React.ReactNode;
hardcodedAttributeKeys?: BaseAutocompleteData[];
operatorConfigKey?: OperatorConfigKeys;
}
export interface Option {
@@ -121,6 +125,7 @@ function QueryBuilderSearchV2(
suffixIcon,
whereClauseConfig,
hardcodedAttributeKeys,
operatorConfigKey,
} = props;
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
@@ -290,7 +295,8 @@ function QueryBuilderSearchV2(
if (
isObject(parsedValue) &&
parsedValue?.key &&
parsedValue?.key?.split(' ').length > 1
parsedValue?.key?.split(' ').length > 1 &&
isLogsDataSource
) {
setTags((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) => {
@@ -689,12 +701,29 @@ function QueryBuilderSearchV2(
})),
);
} else {
setDropdownOptions(
data?.payload?.attributeKeys?.map((key) => ({
setDropdownOptions([
// 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,
value: key,
})) || [],
);
})) || []),
]);
}
}
if (currentState === DropdownState.OPERATOR) {
@@ -717,15 +746,11 @@ function QueryBuilderSearchV2(
op.label.startsWith(partialOperator.toLocaleUpperCase()),
);
}
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
label: operator,
value: operator,
}));
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
} else {
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
(operator) => ({
@@ -739,9 +764,12 @@ function QueryBuilderSearchV2(
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) {
@@ -774,6 +802,7 @@ function QueryBuilderSearchV2(
isLogsDataSource,
searchValue,
suggestionsData?.payload?.attributes,
operatorConfigKey,
]);
// 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 (
<div className="query-builder-search-v2">
<Select
@@ -964,6 +998,7 @@ function QueryBuilderSearchV2(
exampleQueries={suggestionsData?.payload?.example_queries || []}
tags={tags}
currentFilterItem={currentFilterItem}
isLogsDataSource={isLogsDataSource}
/>
)}
>
@@ -990,6 +1025,7 @@ function QueryBuilderSearchV2(
);
})}
</Select>
{isTracesDataSource && <SpanScopeSelector queryName={query.queryName} />}
</div>
);
}
@@ -1000,6 +1036,7 @@ QueryBuilderSearchV2.defaultProps = {
suffixIcon: null,
whereClauseConfig: {},
hardcodedAttributeKeys: undefined,
operatorConfigKey: undefined,
};
export default QueryBuilderSearchV2;

View File

@@ -120,6 +120,7 @@ function SpanScopeSelector({ queryName }: SpanScopeSelectorProps): JSX.Element {
<Select
value={selectedScope}
className="span-scope-selector"
data-testid="span-scope-selector"
onChange={handleScopeChange}
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 { OperatorConfigKeys, OPERATORS_CONFIG } from 'constants/queryBuilder';
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
import { IOption } from 'hooks/useResourceAttribute/types';
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

@@ -230,6 +230,7 @@ export const routesToSkip = [
ROUTES.CHANNELS_NEW,
ROUTES.CHANNELS_EDIT,
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
ROUTES.ALL_ERROR,
];
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 { themeColors } from 'constants/theme';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import SpanLineActionButtons from 'container/TraceWaterfall/SpanLineActionButtons';
import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import {
AlertCircle,
@@ -25,6 +28,7 @@ import {
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import { toFixed } from 'utils/toFixed';
@@ -147,7 +151,7 @@ function SpanOverview({
);
}
function SpanDuration({
export function SpanDuration({
span,
traceMetadata,
setSelectedSpan,
@@ -166,20 +170,40 @@ function SpanDuration({
const leftOffset = ((span.timestamp - traceMetadata.startTime) * 1e2) / spread;
const width = (span.durationNano * 1e2) / (spread * 1e6);
const urlQuery = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
if (span.hasError) {
color = `var(--bg-cherry-500)`;
}
const [hasActionButtons, setHasActionButtons] = useState(false);
const handleMouseEnter = (): void => {
setHasActionButtons(true);
};
const handleMouseLeave = (): void => {
setHasActionButtons(false);
};
return (
<div
className={cx(
'span-duration',
selectedSpan?.spanId === span.spanId ? 'interested-span' : '',
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={(): void => {
setSelectedSpan(span);
if (span?.spanId) {
urlQuery.set('spanId', span?.spanId);
}
safeNavigate({ search: urlQuery.toString() });
}}
>
<div
@@ -190,6 +214,7 @@ function SpanDuration({
backgroundColor: color,
}}
/>
{hasActionButtons && <SpanLineActionButtons span={span} />}
<Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}>
<Typography.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) =>
index ===
self.findIndex(
(o) =>
// to remove duplicate & empty options from list
o.label === option.label &&
o.value === option.value &&
o.dataType?.toLowerCase() === option.dataType?.toLowerCase(), // handle case sensitivity
(o) => o.label === option.label && o.value === option.value, // to remove duplicate & empty options from list
) && option.value !== '',
) || []
).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,
getResourceAttributesTagValues,
} from 'api/metrics/getResourceAttributes';
import { OperatorConversions } from 'constants/resourceAttributes';
import {
CompositeQueryOperatorsConfig,
OperatorConversions,
} from 'constants/resourceAttributes';
import ROUTES from 'constants/routes';
import { MetricsType } from 'container/MetricsApplication/constant';
import {
@@ -49,6 +52,32 @@ export const convertOperatorLabelToTraceOperator = (
OperatorConversions.find((operator) => operator.label === label)
?.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 = (
queries: IResourceAttribute[],
tagType = 'ResourceAttribute',

View File

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

View File

@@ -153,7 +153,7 @@ type Props = {
export const useGetAlertRuleDetails = (): Props => {
const { ruleId } = useAlertHistoryQueryParams();
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
const isValidRuleId = ruleId !== null && ruleId !== '';
const {
isLoading,
@@ -163,7 +163,7 @@ export const useGetAlertRuleDetails = (): Props => {
} = useQuery([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId], {
queryFn: () =>
get({
id: parseInt(ruleId || '', 10),
id: ruleId || '',
}),
enabled: isValidRuleId,
refetchOnWindowFocus: false,
@@ -204,7 +204,7 @@ export const useGetAlertRuleDetailsStats = (): GetAlertRuleDetailsStatsProps =>
{
queryFn: () =>
ruleStats({
id: parseInt(ruleId || '', 10),
id: ruleId || '',
start: startTime,
end: endTime,
}),
@@ -234,7 +234,7 @@ export const useGetAlertRuleDetailsTopContributors = (): GetAlertRuleDetailsTopC
{
queryFn: () =>
topContributors({
id: parseInt(ruleId || '', 10),
id: ruleId || '',
start: startTime,
end: endTime,
}),
@@ -287,7 +287,7 @@ export const useGetAlertRuleDetailsTimelineTable = ({
{
queryFn: () =>
timelineTable({
id: parseInt(ruleId || '', 10),
id: ruleId || '',
start: startTime,
end: endTime,
limit: TIMELINE_TABLE_PAGE_SIZE,
@@ -410,7 +410,7 @@ export const useAlertRuleStatusToggle = ({
const handleAlertStateToggle = (): void => {
const args = {
id: parseInt(ruleId, 10),
id: ruleId,
data: { disabled: alertRuleState !== 'disabled' },
};
toggleAlertState(args);
@@ -512,7 +512,7 @@ export const useAlertRuleUpdate = ({
export const useAlertRuleDelete = ({
ruleId,
}: {
ruleId: number;
ruleId: string;
}): {
handleAlertDelete: () => void;
} => {
@@ -560,7 +560,7 @@ export const useGetAlertRuleDetailsTimelineGraphData = (): GetAlertRuleDetailsTi
{
queryFn: () =>
timelineGraph({
id: parseInt(ruleId || '', 10),
id: ruleId || '',
start: startTime,
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 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 { isNull } from 'lodash-es';
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { routes } from './config';
import { ExceptionsQuickFiltersConfig } from './utils';
function AllErrors(): JSX.Element {
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 (
<>
<ResourceAttributesFilter />
<RouteTab routes={routes} activeKey={pathname} history={history} />
</>
<div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
{showFilters && (
<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: () =>
get({
id: parseInt(ruleId || '', 10),
id: ruleId || '',
}),
enabled: isValidRuleId,
refetchOnMount: false,
@@ -90,10 +90,7 @@ function EditRules(): JSX.Element {
return (
<div className="edit-rules-container">
<EditRulesContainer
ruleId={parseInt(ruleId, 10)}
initialValue={data.payload.data}
/>
<EditRulesContainer ruleId={ruleId || ''} initialValue={data.payload.data} />
</div>
);
}

View File

@@ -143,7 +143,7 @@ export default function Support(): JSX.Element {
});
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', {});
updateCreditCard({
url: window.location.href,
url: window.location.origin,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updateCreditCard]);

View File

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

View File

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

View File

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

View File

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

View File

@@ -6852,18 +6852,17 @@ copy-to-clipboard@^3.3.1, copy-to-clipboard@^3.3.3:
dependencies:
toggle-selection "^1.0.6"
copy-webpack-plugin@^8.1.0:
version "8.1.1"
resolved "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-8.1.1.tgz"
integrity sha512-rYM2uzRxrLRpcyPqGceRBDpxxUV8vcDqIKxAUKfcnFpcrPxT5+XvhTxv7XLjo5AvEJFPdAE3zCogG2JVahqgSQ==
copy-webpack-plugin@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a"
integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==
dependencies:
fast-glob "^3.2.5"
glob-parent "^5.1.1"
globby "^11.0.3"
fast-glob "^3.2.11"
glob-parent "^6.0.1"
globby "^13.1.1"
normalize-path "^3.0.0"
p-limit "^3.1.0"
schema-utils "^3.0.0"
serialize-javascript "^5.0.1"
schema-utils "^4.0.0"
serialize-javascript "^6.0.0"
core-js-compat@^3.25.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"
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"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
@@ -8751,7 +8750,7 @@ fast-glob@^3.0.3:
merge2 "^1.3.0"
micromatch "^4.0.8"
fast-glob@^3.2.5, fast-glob@^3.2.9:
fast-glob@^3.2.9:
version "3.2.12"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz"
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"
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"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
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:
version "0.4.1"
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"
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:
version "1.0.1"
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"
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:
version "0.5.5"
resolved "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz"
@@ -13401,7 +13423,7 @@ p-limit@^2.2.0:
dependencies:
p-try "^2.0.0"
p-limit@^3.0.2, p-limit@^3.1.0:
p-limit@^3.0.2:
version "3.1.0"
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
@@ -15845,17 +15867,10 @@ send@0.19.0:
range-parser "~1.2.1"
statuses "2.0.1"
serialize-javascript@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz"
integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
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==
serialize-javascript@6.0.2, serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
dependencies:
randombytes "^2.1.0"
@@ -16012,6 +16027,11 @@ slash@^3.0.0:
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
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:
version "3.0.0"
resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz"

View File

@@ -3,7 +3,6 @@ package sqlalertmanagerstore
import (
"context"
"database/sql"
"strconv"
"github.com/SigNoz/signoz/pkg/errors"
"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) {
type matcher struct {
bun.BaseModel `bun:"table:rules"`
ID int `bun:"id,pk"`
Data string `bun:"data"`
bun.BaseModel `bun:"table:rule"`
ID valuer.UUID `bun:"id,pk"`
Data string `bun:"data"`
}
matchers := []matcher{}
@@ -213,7 +212,7 @@ func (store *config) GetMatchers(ctx context.Context, orgID string) (map[string]
for _, matcher := range matchers {
receivers := gjson.Get(matcher.Data, "preferredChannels").Array()
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

@@ -160,6 +160,17 @@ func (service *Service) newServer(ctx context.Context, orgID string) (*alertmana
return nil, err
}
beforeCompareAndSelectHash := config.StoreableConfig().Hash
config, err = service.compareAndSelectConfig(ctx, config)
if err != nil {
return nil, err
}
if beforeCompareAndSelectHash == config.StoreableConfig().Hash {
service.settings.Logger().Debug("skipping config store update for org", "orgID", orgID, "hash", config.StoreableConfig().Hash)
return server, nil
}
err = service.configStore.Set(ctx, config)
if err != nil {
return nil, err
@@ -191,6 +202,38 @@ func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmana
return config, nil
}
func (service *Service) compareAndSelectConfig(ctx context.Context, incomingConfig *alertmanagertypes.Config) (*alertmanagertypes.Config, error) {
channels, err := service.configStore.ListChannels(ctx, incomingConfig.StoreableConfig().OrgID)
if err != nil {
return nil, err
}
matchers, err := service.configStore.GetMatchers(ctx, incomingConfig.StoreableConfig().OrgID)
if err != nil {
return nil, err
}
config, err := alertmanagertypes.NewConfigFromChannels(service.config.Global, service.config.Route, channels, incomingConfig.StoreableConfig().OrgID)
if err != nil {
return nil, err
}
for ruleID, receivers := range matchers {
err = config.CreateRuleIDMatcher(ruleID, receivers)
if err != nil {
return nil, err
}
}
if incomingConfig.StoreableConfig().Hash != config.StoreableConfig().Hash {
service.settings.Logger().InfoContext(ctx, "mismatch found, updating config to match channels and matchers")
return config, nil
}
return incomingConfig, nil
}
// getServer returns the server for the given orgID. It should be called with the lock held.
func (service *Service) getServer(orgID string) (*alertmanagerserver.Server, error) {
server, ok := service.servers[orgID]

122
pkg/apis/fields/api.go Normal file
View File

@@ -0,0 +1,122 @@
package fields
import (
"bytes"
"io"
"net/http"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/telemetrymetadata"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrytraces"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"go.uber.org/zap"
)
type API struct {
telemetryStore telemetrystore.TelemetryStore
telemetryMetadataStore telemetrytypes.MetadataStore
}
func NewAPI(telemetryStore telemetrystore.TelemetryStore) *API {
telemetryMetadataStore := telemetrymetadata.NewTelemetryMetaStore(
telemetryStore,
telemetrytraces.DBName,
telemetrytraces.TagAttributesV2TableName,
telemetrytraces.SpanIndexV3TableName,
telemetrymetrics.DBName,
telemetrymetrics.TimeseriesV41weekTableName,
telemetrymetrics.TimeseriesV41weekLocalTableName,
telemetrylogs.DBName,
telemetrylogs.LogsV2TableName,
telemetrylogs.TagAttributesV2TableName,
telemetrymetadata.DBName,
telemetrymetadata.AttributesMetadataLocalTableName,
)
return &API{
telemetryStore: telemetryStore,
telemetryMetadataStore: telemetryMetadataStore,
}
}
func (api *API) GetFieldsKeys(w http.ResponseWriter, r *http.Request) {
type fieldKeysResponse struct {
Keys map[string][]*telemetrytypes.TelemetryFieldKey `json:"keys"`
Complete bool `json:"complete"`
}
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
ctx := r.Context()
fieldKeySelector, err := parseFieldKeyRequest(r)
if err != nil {
render.Error(w, err)
return
}
keys, err := api.telemetryMetadataStore.GetKeys(ctx, fieldKeySelector)
if err != nil {
render.Error(w, err)
return
}
response := fieldKeysResponse{
Keys: keys,
Complete: len(keys) < fieldKeySelector.Limit,
}
render.Success(w, http.StatusOK, response)
}
func (api *API) GetFieldsValues(w http.ResponseWriter, r *http.Request) {
type fieldValuesResponse struct {
Values *telemetrytypes.TelemetryFieldValues `json:"values"`
Complete bool `json:"complete"`
}
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
ctx := r.Context()
fieldValueSelector, err := parseFieldValueRequest(r)
if err != nil {
render.Error(w, err)
return
}
allValues, err := api.telemetryMetadataStore.GetAllValues(ctx, fieldValueSelector)
if err != nil {
render.Error(w, err)
return
}
relatedValues, err := api.telemetryMetadataStore.GetRelatedValues(ctx, fieldValueSelector)
if err != nil {
// we don't want to return error if we fail to get related values for some reason
zap.L().Error("failed to get related values", zap.Error(err))
relatedValues = []string{}
}
values := &telemetrytypes.TelemetryFieldValues{
StringValues: allValues.StringValues,
NumberValues: allValues.NumberValues,
RelatedValues: relatedValues,
}
response := fieldValuesResponse{
Values: values,
Complete: len(values.StringValues) < fieldValueSelector.Limit &&
len(values.BoolValues) < fieldValueSelector.Limit &&
len(values.NumberValues) < fieldValueSelector.Limit &&
len(values.RelatedValues) < fieldValueSelector.Limit,
}
render.Success(w, http.StatusOK, response)
}

112
pkg/apis/fields/parse.go Normal file
View File

@@ -0,0 +1,112 @@
package fields
import (
"net/http"
"strconv"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, error) {
var req telemetrytypes.FieldKeySelector
var signal telemetrytypes.Signal
var err error
signalStr := r.URL.Query().Get("signal")
if signalStr != "" {
signal = telemetrytypes.Signal{String: valuer.NewString(signalStr)}
} else {
signal = telemetrytypes.SignalUnspecified
}
if r.URL.Query().Get("limit") != "" {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse limit")
}
req.Limit = limit
} else {
req.Limit = 1000
}
var startUnixMilli, endUnixMilli int64
if r.URL.Query().Get("startUnixMilli") != "" {
startUnixMilli, err := strconv.ParseInt(r.URL.Query().Get("startUnixMilli"), 10, 64)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse startUnixMilli")
}
// Round down to the nearest 6 hours (21600000 milliseconds)
startUnixMilli -= startUnixMilli % 21600000
}
if r.URL.Query().Get("endUnixMilli") != "" {
endUnixMilli, err = strconv.ParseInt(r.URL.Query().Get("endUnixMilli"), 10, 64)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse endUnixMilli")
}
}
// Parse fieldContext directly instead of using JSON unmarshalling.
var fieldContext telemetrytypes.FieldContext
fieldContextStr := r.URL.Query().Get("fieldContext")
if fieldContextStr != "" {
fieldContext = telemetrytypes.FieldContext{String: valuer.NewString(fieldContextStr)}
}
// Parse fieldDataType directly instead of using JSON unmarshalling.
var fieldDataType telemetrytypes.FieldDataType
fieldDataTypeStr := r.URL.Query().Get("fieldDataType")
if fieldDataTypeStr != "" {
fieldDataType = telemetrytypes.FieldDataType{String: valuer.NewString(fieldDataTypeStr)}
}
metricName := r.URL.Query().Get("metricName")
var metricContext *telemetrytypes.MetricContext
if metricName != "" {
metricContext = &telemetrytypes.MetricContext{
MetricName: metricName,
}
}
name := r.URL.Query().Get("name")
req = telemetrytypes.FieldKeySelector{
StartUnixMilli: startUnixMilli,
EndUnixMilli: endUnixMilli,
Signal: signal,
Name: name,
FieldContext: fieldContext,
FieldDataType: fieldDataType,
Limit: req.Limit,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: metricContext,
}
return &req, nil
}
func parseFieldValueRequest(r *http.Request) (*telemetrytypes.FieldValueSelector, error) {
keySelector, err := parseFieldKeyRequest(r)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse field key request")
}
existingQuery := r.URL.Query().Get("existingQuery")
value := r.URL.Query().Get("value")
// Parse limit for fieldValue request, fallback to default 50 if parsing fails.
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
limit = 50
}
req := telemetrytypes.FieldValueSelector{
FieldKeySelector: keySelector,
ExistingQuery: existingQuery,
Value: value,
Limit: limit,
}
return &req, nil
}

View File

@@ -19,6 +19,7 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/preference"
@@ -59,6 +60,7 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"go.uber.org/zap"
@@ -141,6 +143,8 @@ type APIHandler struct {
AlertmanagerAPI *alertmanager.API
FieldsAPI *fields.API
Signoz *signoz.SigNoz
Preference preference.API
@@ -188,6 +192,8 @@ type APIHandlerOpts struct {
AlertmanagerAPI *alertmanager.API
FieldsAPI *fields.API
Signoz *signoz.SigNoz
Preference preference.API
@@ -260,6 +266,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
AlertmanagerAPI: opts.AlertmanagerAPI,
Signoz: opts.Signoz,
Preference: opts.Preference,
FieldsAPI: opts.FieldsAPI,
}
logsQueryBuilder := logsv3.PrepareLogsQuery
@@ -416,6 +423,13 @@ func (aH *APIHandler) RegisterQueryRangeV3Routes(router *mux.Router, am *AuthMid
subRouter.HandleFunc("/logs/livetail", am.ViewAccess(aH.liveTailLogs)).Methods(http.MethodGet)
}
func (aH *APIHandler) RegisterFieldsRoutes(router *mux.Router, am *AuthMiddleware) {
subRouter := router.PathPrefix("/api/v1").Subrouter()
subRouter.HandleFunc("/fields/keys", am.ViewAccess(aH.FieldsAPI.GetFieldsKeys)).Methods(http.MethodGet)
subRouter.HandleFunc("/fields/values", am.ViewAccess(aH.FieldsAPI.GetFieldsValues)).Methods(http.MethodGet)
}
func (aH *APIHandler) RegisterInfraMetricsRoutes(router *mux.Router, am *AuthMiddleware) {
hostsSubRouter := router.PathPrefix("/api/v1/hosts").Subrouter()
hostsSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getHostAttributeKeys)).Methods(http.MethodGet)
@@ -734,9 +748,15 @@ func (aH *APIHandler) PopulateTemporality(ctx context.Context, qp *v3.QueryRange
}
func (aH *APIHandler) listDowntimeSchedules(w http.ResponseWriter, r *http.Request) {
schedules, err := aH.ruleManager.RuleDB().GetAllPlannedMaintenance(r.Context())
claims, ok := authtypes.ClaimsFromContext(r.Context())
if !ok {
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
return
}
schedules, err := aH.ruleManager.MaintenanceStore().GetAllPlannedMaintenance(r.Context(), claims.OrgID)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
render.Error(w, err)
return
}
@@ -744,7 +764,7 @@ func (aH *APIHandler) listDowntimeSchedules(w http.ResponseWriter, r *http.Reque
// Since the number of schedules is expected to be small, this should be fine
if r.URL.Query().Get("active") != "" {
activeSchedules := make([]rules.PlannedMaintenance, 0)
activeSchedules := make([]*ruletypes.GettablePlannedMaintenance, 0)
active, _ := strconv.ParseBool(r.URL.Query().Get("active"))
for _, schedule := range schedules {
now := time.Now().In(time.FixedZone(schedule.Schedule.Timezone, 0))
@@ -756,7 +776,7 @@ func (aH *APIHandler) listDowntimeSchedules(w http.ResponseWriter, r *http.Reque
}
if r.URL.Query().Get("recurring") != "" {
recurringSchedules := make([]rules.PlannedMaintenance, 0)
recurringSchedules := make([]*ruletypes.GettablePlannedMaintenance, 0)
recurring, _ := strconv.ParseBool(r.URL.Query().Get("recurring"))
for _, schedule := range schedules {
if schedule.IsRecurring() == recurring {
@@ -770,62 +790,83 @@ func (aH *APIHandler) listDowntimeSchedules(w http.ResponseWriter, r *http.Reque
}
func (aH *APIHandler) getDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
schedule, err := aH.ruleManager.RuleDB().GetPlannedMaintenanceByID(r.Context(), id)
idStr := mux.Vars(r)["id"]
id, err := valuer.NewUUID(idStr)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()))
return
}
schedule, err := aH.ruleManager.MaintenanceStore().GetPlannedMaintenanceByID(r.Context(), id)
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, schedule)
}
func (aH *APIHandler) createDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
var schedule rules.PlannedMaintenance
var schedule ruletypes.GettablePlannedMaintenance
err := json.NewDecoder(r.Body).Decode(&schedule)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
if err := schedule.Validate(); err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
render.Error(w, err)
return
}
_, err = aH.ruleManager.RuleDB().CreatePlannedMaintenance(r.Context(), schedule)
_, err = aH.ruleManager.MaintenanceStore().CreatePlannedMaintenance(r.Context(), schedule)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
render.Error(w, err)
return
}
aH.Respond(w, nil)
}
func (aH *APIHandler) editDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var schedule rules.PlannedMaintenance
err := json.NewDecoder(r.Body).Decode(&schedule)
idStr := mux.Vars(r)["id"]
id, err := valuer.NewUUID(idStr)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()))
return
}
var schedule ruletypes.GettablePlannedMaintenance
err = json.NewDecoder(r.Body).Decode(&schedule)
if err != nil {
render.Error(w, err)
return
}
if err := schedule.Validate(); err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
render.Error(w, err)
return
}
_, err = aH.ruleManager.RuleDB().EditPlannedMaintenance(r.Context(), schedule, id)
err = aH.ruleManager.MaintenanceStore().EditPlannedMaintenance(r.Context(), schedule, id)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
render.Error(w, err)
return
}
aH.Respond(w, nil)
}
func (aH *APIHandler) deleteDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
_, err := aH.ruleManager.RuleDB().DeletePlannedMaintenance(r.Context(), id)
idStr := mux.Vars(r)["id"]
id, err := valuer.NewUUID(idStr)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()))
return
}
err = aH.ruleManager.MaintenanceStore().DeletePlannedMaintenance(r.Context(), id)
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, nil)
}
@@ -929,12 +970,12 @@ func (aH *APIHandler) getOverallStateTransitions(w http.ResponseWriter, r *http.
aH.Respond(w, stateItems)
}
func (aH *APIHandler) metaForLinks(ctx context.Context, rule *rules.GettableRule) ([]v3.FilterItem, []v3.AttributeKey, map[string]v3.AttributeKey) {
func (aH *APIHandler) metaForLinks(ctx context.Context, rule *ruletypes.GettableRule) ([]v3.FilterItem, []v3.AttributeKey, map[string]v3.AttributeKey) {
filterItems := []v3.FilterItem{}
groupBy := []v3.AttributeKey{}
keys := make(map[string]v3.AttributeKey)
if rule.AlertType == rules.AlertTypeLogs {
if rule.AlertType == ruletypes.AlertTypeLogs {
logFields, err := aH.reader.GetLogFields(ctx)
if err == nil {
params := &v3.QueryRangeParamsV3{
@@ -944,7 +985,7 @@ func (aH *APIHandler) metaForLinks(ctx context.Context, rule *rules.GettableRule
} else {
zap.L().Error("failed to get log fields using empty keys; the link might not work as expected", zap.Error(err))
}
} else if rule.AlertType == rules.AlertTypeTraces {
} else if rule.AlertType == ruletypes.AlertTypeTraces {
traceFields, err := aH.reader.GetSpanAttributeKeys(ctx)
if err == nil {
keys = traceFields
@@ -953,7 +994,7 @@ func (aH *APIHandler) metaForLinks(ctx context.Context, rule *rules.GettableRule
}
}
if rule.AlertType == rules.AlertTypeLogs || rule.AlertType == rules.AlertTypeTraces {
if rule.AlertType == ruletypes.AlertTypeLogs || rule.AlertType == ruletypes.AlertTypeTraces {
if rule.RuleCondition.CompositeQuery != nil {
if rule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
selectedQuery := rule.RuleCondition.GetSelectedQueryName()
@@ -1006,9 +1047,9 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request
// alerts have 2 minutes delay built in, so we need to subtract that from the start time
// to get the correct query range
start := end.Add(-time.Duration(rule.EvalWindow)).Add(-3 * time.Minute)
if rule.AlertType == rules.AlertTypeLogs {
if rule.AlertType == ruletypes.AlertTypeLogs {
res.Items[idx].RelatedLogsLink = contextlinks.PrepareLinksToLogs(start, end, newFilters)
} else if rule.AlertType == rules.AlertTypeTraces {
} else if rule.AlertType == ruletypes.AlertTypeTraces {
res.Items[idx].RelatedTracesLink = contextlinks.PrepareLinksToTraces(start, end, newFilters)
}
}
@@ -1044,9 +1085,9 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter,
newFilters := contextlinks.PrepareFilters(lbls, filterItems, groupBy, keys)
end := time.Unix(params.End/1000, 0)
start := time.Unix(params.Start/1000, 0)
if rule.AlertType == rules.AlertTypeLogs {
if rule.AlertType == ruletypes.AlertTypeLogs {
res[idx].RelatedLogsLink = contextlinks.PrepareLinksToLogs(start, end, newFilters)
} else if rule.AlertType == rules.AlertTypeTraces {
} else if rule.AlertType == ruletypes.AlertTypeTraces {
res[idx].RelatedTracesLink = contextlinks.PrepareLinksToTraces(start, end, newFilters)
}
}
@@ -5559,7 +5600,12 @@ func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
return
}
result = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
result, err = postprocess.PostProcessResult(result, queryRangeParams)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
if !thirdPartyQueryRequest.ShowIp {
result = thirdPartyApi.FilterResponse(result)

View File

@@ -7,11 +7,11 @@ import (
"strings"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -34,7 +34,7 @@ type IntegrationAssets struct {
Logs LogsAssets `json:"logs"`
Dashboards []types.DashboardData `json:"dashboards"`
Alerts []rules.PostableRule `json:"alerts"`
Alerts []ruletypes.PostableRule `json:"alerts"`
}
type LogsAssets struct {

View File

@@ -10,10 +10,10 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/dao"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/google/uuid"
)
@@ -129,7 +129,7 @@ func (t *TestAvailableIntegrationsRepo) list(
},
},
Dashboards: []types.DashboardData{},
Alerts: []rules.PostableRule{},
Alerts: []ruletypes.PostableRule{},
},
ConnectionTests: &IntegrationConnectionTests{
Logs: &LogsConnectionTest{
@@ -197,7 +197,7 @@ func (t *TestAvailableIntegrationsRepo) list(
},
},
Dashboards: []types.DashboardData{},
Alerts: []rules.PostableRule{},
Alerts: []ruletypes.PostableRule{},
},
ConnectionTests: &IntegrationConnectionTests{
Logs: &LogsConnectionTest{

View File

@@ -58,6 +58,7 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
builderQueries["endpoints"] = &v3.BuilderQuery{
QueryName: "endpoints",
Legend: "endpoints",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorCountDistinct,
@@ -91,11 +92,14 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
ReduceTo: v3.ReduceToOperatorAvg,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["lastseen"] = &v3.BuilderQuery{
QueryName: "lastseen",
Legend: "lastseen",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorMax,
@@ -127,11 +131,14 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
ReduceTo: v3.ReduceToOperatorAvg,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["rps"] = &v3.BuilderQuery{
QueryName: "rps",
Legend: "rps",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorRate,
@@ -163,18 +170,22 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
ReduceTo: v3.ReduceToOperatorAvg,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["error_rate"] = &v3.BuilderQuery{
QueryName: "error_rate",
builderQueries["error"] = &v3.BuilderQuery{
QueryName: "error",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorRate,
AggregateOperator: v3.AggregateOperatorCountDistinct,
AggregateAttribute: v3.AttributeKey{
Key: "",
Key: "span_id",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
TimeAggregation: v3.TimeAggregationRate,
TimeAggregation: v3.TimeAggregationCountDistinct,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
@@ -200,7 +211,7 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
},
}, thirdPartyApis.Filters),
},
Expression: "error_rate",
Expression: "error",
GroupBy: getGroupBy([]v3.AttributeKey{
{
Key: "net.peer.name",
@@ -208,11 +219,56 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
ReduceTo: v3.ReduceToOperatorAvg,
Disabled: true,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["total_span"] = &v3.BuilderQuery{
QueryName: "total_span",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorCountDistinct,
AggregateAttribute: v3.AttributeKey{
Key: "span_id",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
TimeAggregation: v3.TimeAggregationCountDistinct,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "http.url",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
}, thirdPartyApis.Filters),
},
Expression: "total_span",
GroupBy: getGroupBy([]v3.AttributeKey{
{
Key: "net.peer.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
Disabled: true,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["p99"] = &v3.BuilderQuery{
QueryName: "p99",
Legend: "p99",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorP99,
@@ -246,7 +302,18 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
ReduceTo: v3.ReduceToOperatorAvg,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["error_rate"] = &v3.BuilderQuery{
QueryName: "error_rate",
Expression: "(error/total_span)*100",
Legend: "error_rate",
Disabled: false,
ShiftBy: 0,
IsAnomaly: false,
}
compositeQuery := &v3.CompositeQuery{

View File

@@ -26,6 +26,7 @@ var AggregateOperatorToSQLFunc = map[v3.AggregateOperator]string{
v3.AggregateOperatorMax: "max",
v3.AggregateOperatorMin: "min",
v3.AggregateOperatorSum: "sum",
v3.AggregateOperatorRate: "count",
v3.AggregateOperatorRateSum: "sum",
v3.AggregateOperatorRateAvg: "avg",
v3.AggregateOperatorRateMax: "max",

View File

@@ -282,7 +282,7 @@ func orderByAttributeKeyTags(panelType v3.PanelType, items []v3.OrderBy, tags []
return str
}
func generateAggregateClause(aggOp v3.AggregateOperator,
func generateAggregateClause(panelType v3.PanelType, start, end int64, aggOp v3.AggregateOperator,
aggKey string,
step int64,
timeFilter string,
@@ -296,18 +296,20 @@ func generateAggregateClause(aggOp v3.AggregateOperator,
"%s%s" +
"%s"
switch aggOp {
case v3.AggregateOperatorRate:
rate := float64(step)
op := fmt.Sprintf("count(%s)/%f", aggKey, rate)
query := fmt.Sprintf(queryTmpl, op, whereClause, groupBy, having, orderBy)
return query, nil
case
v3.AggregateOperatorRateSum,
v3.AggregateOperatorRateMax,
v3.AggregateOperatorRateAvg,
v3.AggregateOperatorRateMin:
v3.AggregateOperatorRateMin,
v3.AggregateOperatorRate:
rate := float64(step)
if panelType == v3.PanelTypeTable {
// if the panel type is table the denominator will be the total time range
duration := end - start
if duration >= 0 {
rate = float64(duration) / NANOSECOND
}
}
op := fmt.Sprintf("%s(%s)/%f", logsV3.AggregateOperatorToSQLFunc[aggOp], aggKey, rate)
query := fmt.Sprintf(queryTmpl, op, whereClause, groupBy, having, orderBy)
@@ -418,7 +420,7 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build
filterSubQuery = filterSubQuery + " AND " + fmt.Sprintf("(%s) GLOBAL IN (", logsV3.GetSelectKeys(mq.AggregateOperator, mq.GroupBy)) + "#LIMIT_PLACEHOLDER)"
}
aggClause, err := generateAggregateClause(mq.AggregateOperator, aggregationKey, step, timeFilter, filterSubQuery, groupBy, having, orderBy)
aggClause, err := generateAggregateClause(panelType, logsStart, logsEnd, mq.AggregateOperator, aggregationKey, step, timeFilter, filterSubQuery, groupBy, having, orderBy)
if err != nil {
return "", err
}

View File

@@ -571,6 +571,9 @@ func Test_orderByAttributeKeyTags(t *testing.T) {
func Test_generateAggregateClause(t *testing.T) {
type args struct {
panelType v3.PanelType
start int64
end int64
op v3.AggregateOperator
aggKey string
step int64
@@ -590,6 +593,7 @@ func Test_generateAggregateClause(t *testing.T) {
name: "test rate",
args: args{
op: v3.AggregateOperatorRate,
panelType: v3.PanelTypeGraph,
aggKey: "test",
step: 60,
timeFilter: "(timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND (ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458)",
@@ -606,6 +610,7 @@ func Test_generateAggregateClause(t *testing.T) {
name: "test P10 with all args",
args: args{
op: v3.AggregateOperatorRate,
panelType: v3.PanelTypeGraph,
aggKey: "test",
step: 60,
timeFilter: "(timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND (ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458)",
@@ -618,10 +623,29 @@ func Test_generateAggregateClause(t *testing.T) {
"(ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458) AND attributes_string['service.name'] = 'test' group by `user_name` having value > 10 order by " +
"`user_name` desc",
},
{
name: "test rate for table panel",
args: args{
op: v3.AggregateOperatorRate,
panelType: v3.PanelTypeTable,
start: 1745315470000000000,
end: 1745319070000000000,
aggKey: "test",
step: 60,
timeFilter: "(timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND (ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458)",
whereClause: " AND attributes_string['service.name'] = 'test'",
groupBy: " group by `user_name`",
having: "",
orderBy: " order by `user_name` desc",
},
want: " count(test)/3600.000000 as value from signoz_logs.distributed_logs_v2 where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND " +
"(ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458) AND attributes_string['service.name'] = 'test' " +
"group by `user_name` order by `user_name` desc",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := generateAggregateClause(tt.args.op, tt.args.aggKey, tt.args.step, tt.args.timeFilter, tt.args.whereClause, tt.args.groupBy, tt.args.having, tt.args.orderBy)
got, err := generateAggregateClause(tt.args.panelType, tt.args.start, tt.args.end, tt.args.op, tt.args.aggKey, tt.args.step, tt.args.timeFilter, tt.args.whereClause, tt.args.groupBy, tt.args.having, tt.args.orderBy)
if (err != nil) != tt.wantErr {
t.Errorf("generateAggreagteClause() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@@ -13,6 +13,7 @@ import (
"github.com/jmoiron/sqlx"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/preference"
preferencecore "github.com/SigNoz/signoz/pkg/modules/preference/core"
@@ -201,6 +202,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
JWT: serverOptions.Jwt,
AlertmanagerAPI: alertmanager.NewAPI(serverOptions.SigNoz.Alertmanager),
FieldsAPI: fields.NewAPI(serverOptions.SigNoz.TelemetryStore),
Signoz: serverOptions.SigNoz,
Preference: preferenceModule,
})
@@ -321,6 +323,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
api.RegisterLogsRoutes(r, am)
api.RegisterIntegrationRoutes(r, am)
api.RegisterCloudIntegrationsRoutes(r, am)
api.RegisterFieldsRoutes(r, am)
api.RegisterQueryRangeV3Routes(r, am)
api.RegisterInfraMetricsRoutes(r, am)
api.RegisterWebSocketPaths(r, am)
@@ -382,11 +385,11 @@ func (s *Server) initListeners() error {
}
// 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
if !s.serverOptions.DisableRules {
s.ruleManager.Start()
s.ruleManager.Start(ctx)
} else {
zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE")
}
@@ -454,7 +457,7 @@ func (s *Server) Start() error {
return nil
}
func (s *Server) Stop() error {
func (s *Server) Stop(ctx context.Context) error {
if s.httpServer != nil {
if err := s.httpServer.Shutdown(context.Background()); err != nil {
return err
@@ -470,7 +473,7 @@ func (s *Server) Stop() error {
s.opampServer.Stop()
if s.ruleManager != nil {
s.ruleManager.Stop()
s.ruleManager.Stop(ctx)
}
return nil

View File

@@ -367,8 +367,15 @@ func buildTracesQuery(start, end, step int64, mq *v3.BuilderQuery, panelType v3.
v3.AggregateOperatorRateAvg,
v3.AggregateOperatorRateMin,
v3.AggregateOperatorRate:
rate := float64(step)
if panelType == v3.PanelTypeTable {
// if the panel type is table the denominator will be the total time range
duration := tracesEnd - tracesStart
if duration >= 0 {
rate = float64(duration) / NANOSECOND
}
}
op := fmt.Sprintf("%s(%s)/%f", tracesV3.AggregateOperatorToSQLFunc[mq.AggregateOperator], aggregationKey, rate)
query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy)
return query, nil

View File

@@ -693,6 +693,38 @@ func Test_buildTracesQuery(t *testing.T) {
want: "SELECT max(toUnixTimestamp64Nano(timestamp)) as value from signoz_traces.distributed_signoz_index_v3 where (timestamp >= '1680066360726210000' AND timestamp <= '1680066458000000000') AND " +
"(ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458) order by value DESC",
},
{
name: "test rate for graph panel",
args: args{
panelType: v3.PanelTypeGraph,
start: 1745315470000000000,
end: 1745319070000000000,
step: 60,
mq: &v3.BuilderQuery{
AggregateOperator: v3.AggregateOperatorRate,
StepInterval: 60,
Filters: &v3.FilterSet{},
},
},
want: "SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, count()/60.000000 as value from signoz_traces.distributed_signoz_index_v3 where (timestamp >= '1745315470000000000' AND " +
"timestamp <= '1745319070000000000') AND (ts_bucket_start >= 1745313670 AND ts_bucket_start <= 1745319070) group by ts order by value DESC",
},
{
name: "test rate for table panel",
args: args{
panelType: v3.PanelTypeTable,
start: 1745315470000000000,
end: 1745319070000000000,
step: 60,
mq: &v3.BuilderQuery{
AggregateOperator: v3.AggregateOperatorRate,
StepInterval: 60,
Filters: &v3.FilterSet{},
},
},
want: "SELECT count()/3600.000000 as value from signoz_traces.distributed_signoz_index_v3 where (timestamp >= '1745315470000000000' AND " +
"timestamp <= '1745319070000000000') AND (ts_bucket_start >= 1745313670 AND ts_bucket_start <= 1745319070) order by value DESC",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

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

View File

@@ -14,6 +14,7 @@ import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
qslabels "github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/sqlstore"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"go.uber.org/zap"
)
@@ -25,9 +26,9 @@ type BaseRule struct {
handledRestart bool
// Type of the rule
typ AlertType
typ ruletypes.AlertType
ruleCondition *RuleCondition
ruleCondition *ruletypes.RuleCondition
// evalWindow is the time window used for evaluating the rule
// i.e each time we lookback from the current time, we look at data for the last
// evalWindow duration
@@ -52,9 +53,9 @@ type BaseRule struct {
// the timestamp of the last evaluation
evaluationTimestamp time.Time
health RuleHealth
health ruletypes.RuleHealth
lastError error
Active map[uint64]*Alert
Active map[uint64]*ruletypes.Alert
// lastTimestampWithDatapoints is the timestamp of the last datapoint we observed
// for this rule
@@ -115,7 +116,7 @@ func WithSQLStore(sqlstore sqlstore.SQLStore) RuleOption {
}
}
func NewBaseRule(id string, p *PostableRule, reader interfaces.Reader, opts ...RuleOption) (*BaseRule, error) {
func NewBaseRule(id string, p *ruletypes.PostableRule, reader interfaces.Reader, opts ...RuleOption) (*BaseRule, error) {
if p.RuleCondition == nil || !p.RuleCondition.IsValid() {
return nil, fmt.Errorf("invalid rule condition")
}
@@ -130,8 +131,8 @@ func NewBaseRule(id string, p *PostableRule, reader interfaces.Reader, opts ...R
labels: qslabels.FromMap(p.Labels),
annotations: qslabels.FromMap(p.Annotations),
preferredChannels: p.PreferredChannels,
health: HealthUnknown,
Active: map[uint64]*Alert{},
health: ruletypes.HealthUnknown,
Active: map[uint64]*ruletypes.Alert{},
reader: reader,
TemporalityMap: make(map[string]map[v3.Temporality]bool),
}
@@ -163,25 +164,25 @@ func (r *BaseRule) targetVal() float64 {
return value.F
}
func (r *BaseRule) matchType() MatchType {
func (r *BaseRule) matchType() ruletypes.MatchType {
if r.ruleCondition == nil {
return AtleastOnce
return ruletypes.AtleastOnce
}
return r.ruleCondition.MatchType
}
func (r *BaseRule) compareOp() CompareOp {
func (r *BaseRule) compareOp() ruletypes.CompareOp {
if r.ruleCondition == nil {
return ValueIsEq
return ruletypes.ValueIsEq
}
return r.ruleCondition.CompareOp
}
func (r *BaseRule) currentAlerts() []*Alert {
func (r *BaseRule) currentAlerts() []*ruletypes.Alert {
r.mtx.Lock()
defer r.mtx.Unlock()
alerts := make([]*Alert, 0, len(r.Active))
alerts := make([]*ruletypes.Alert, 0, len(r.Active))
for _, a := range r.Active {
anew := *a
alerts = append(alerts, &anew)
@@ -216,15 +217,15 @@ func (r *ThresholdRule) hostFromSource() string {
return fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Hostname())
}
func (r *BaseRule) ID() string { return r.id }
func (r *BaseRule) Name() string { return r.name }
func (r *BaseRule) Condition() *RuleCondition { return r.ruleCondition }
func (r *BaseRule) Labels() qslabels.BaseLabels { return r.labels }
func (r *BaseRule) Annotations() qslabels.BaseLabels { return r.annotations }
func (r *BaseRule) PreferredChannels() []string { return r.preferredChannels }
func (r *BaseRule) ID() string { return r.id }
func (r *BaseRule) Name() string { return r.name }
func (r *BaseRule) Condition() *ruletypes.RuleCondition { return r.ruleCondition }
func (r *BaseRule) Labels() qslabels.BaseLabels { return r.labels }
func (r *BaseRule) Annotations() qslabels.BaseLabels { return r.annotations }
func (r *BaseRule) PreferredChannels() []string { return r.preferredChannels }
func (r *BaseRule) GeneratorURL() string {
return prepareRuleGeneratorURL(r.ID(), r.source)
return ruletypes.PrepareRuleGeneratorURL(r.ID(), r.source)
}
func (r *BaseRule) Unit() string {
@@ -261,13 +262,13 @@ func (r *BaseRule) LastError() error {
return r.lastError
}
func (r *BaseRule) SetHealth(health RuleHealth) {
func (r *BaseRule) SetHealth(health ruletypes.RuleHealth) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.health = health
}
func (r *BaseRule) Health() RuleHealth {
func (r *BaseRule) Health() ruletypes.RuleHealth {
r.mtx.Lock()
defer r.mtx.Unlock()
return r.health
@@ -307,8 +308,8 @@ func (r *BaseRule) State() model.AlertState {
return maxState
}
func (r *BaseRule) ActiveAlerts() []*Alert {
var res []*Alert
func (r *BaseRule) ActiveAlerts() []*ruletypes.Alert {
var res []*ruletypes.Alert
for _, a := range r.currentAlerts() {
if a.ResolvedAt.IsZero() {
res = append(res, a)
@@ -332,9 +333,9 @@ func (r *BaseRule) SendAlerts(ctx context.Context, ts time.Time, resendDelay tim
return
}
alerts := []*Alert{}
r.ForEachActiveAlert(func(alert *Alert) {
if alert.needsSending(ts, resendDelay) {
alerts := []*ruletypes.Alert{}
r.ForEachActiveAlert(func(alert *ruletypes.Alert) {
if alert.NeedsSending(ts, resendDelay) {
alert.LastSentAt = ts
delta := resendDelay
if interval > resendDelay {
@@ -348,7 +349,7 @@ func (r *BaseRule) SendAlerts(ctx context.Context, ts time.Time, resendDelay tim
notifyFunc(ctx, orgID, "", alerts...)
}
func (r *BaseRule) ForEachActiveAlert(f func(*Alert)) {
func (r *BaseRule) ForEachActiveAlert(f func(*ruletypes.Alert)) {
r.mtx.Lock()
defer r.mtx.Unlock()
@@ -357,8 +358,8 @@ func (r *BaseRule) ForEachActiveAlert(f func(*Alert)) {
}
}
func (r *BaseRule) ShouldAlert(series v3.Series) (Sample, bool) {
var alertSmpl Sample
func (r *BaseRule) ShouldAlert(series v3.Series) (ruletypes.Sample, bool) {
var alertSmpl ruletypes.Sample
var shouldAlert bool
var lbls qslabels.Labels
@@ -381,54 +382,54 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (Sample, bool) {
}
switch r.matchType() {
case AtleastOnce:
case ruletypes.AtleastOnce:
// If any sample matches the condition, the rule is firing.
if r.compareOp() == ValueIsAbove {
if r.compareOp() == ruletypes.ValueIsAbove {
for _, smpl := range series.Points {
if smpl.Value > r.targetVal() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
} else if r.compareOp() == ValueIsBelow {
} else if r.compareOp() == ruletypes.ValueIsBelow {
for _, smpl := range series.Points {
if smpl.Value < r.targetVal() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
} else if r.compareOp() == ValueIsEq {
} else if r.compareOp() == ruletypes.ValueIsEq {
for _, smpl := range series.Points {
if smpl.Value == r.targetVal() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
} else if r.compareOp() == ValueIsNotEq {
} else if r.compareOp() == ruletypes.ValueIsNotEq {
for _, smpl := range series.Points {
if smpl.Value != r.targetVal() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
} else if r.compareOp() == ValueOutsideBounds {
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
for _, smpl := range series.Points {
if math.Abs(smpl.Value) >= r.targetVal() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = true
break
}
}
}
case AllTheTimes:
case ruletypes.AllTheTimes:
// If all samples match the condition, the rule is firing.
shouldAlert = true
alertSmpl = Sample{Point: Point{V: r.targetVal()}, Metric: lbls}
if r.compareOp() == ValueIsAbove {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: r.targetVal()}, Metric: lbls}
if r.compareOp() == ruletypes.ValueIsAbove {
for _, smpl := range series.Points {
if smpl.Value <= r.targetVal() {
shouldAlert = false
@@ -443,9 +444,9 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (Sample, bool) {
minValue = smpl.Value
}
}
alertSmpl = Sample{Point: Point{V: minValue}, Metric: lbls}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: minValue}, Metric: lbls}
}
} else if r.compareOp() == ValueIsBelow {
} else if r.compareOp() == ruletypes.ValueIsBelow {
for _, smpl := range series.Points {
if smpl.Value >= r.targetVal() {
shouldAlert = false
@@ -459,16 +460,16 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (Sample, bool) {
maxValue = smpl.Value
}
}
alertSmpl = Sample{Point: Point{V: maxValue}, Metric: lbls}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: maxValue}, Metric: lbls}
}
} else if r.compareOp() == ValueIsEq {
} else if r.compareOp() == ruletypes.ValueIsEq {
for _, smpl := range series.Points {
if smpl.Value != r.targetVal() {
shouldAlert = false
break
}
}
} else if r.compareOp() == ValueIsNotEq {
} else if r.compareOp() == ruletypes.ValueIsNotEq {
for _, smpl := range series.Points {
if smpl.Value == r.targetVal() {
shouldAlert = false
@@ -479,21 +480,21 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (Sample, bool) {
if shouldAlert {
for _, smpl := range series.Points {
if !math.IsInf(smpl.Value, 0) && !math.IsNaN(smpl.Value) {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
break
}
}
}
} else if r.compareOp() == ValueOutsideBounds {
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
for _, smpl := range series.Points {
if math.Abs(smpl.Value) < r.targetVal() {
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
shouldAlert = false
break
}
}
}
case OnAverage:
case ruletypes.OnAverage:
// If the average of all samples matches the condition, the rule is firing.
var sum, count float64
for _, smpl := range series.Points {
@@ -504,29 +505,29 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (Sample, bool) {
count++
}
avg := sum / count
alertSmpl = Sample{Point: Point{V: avg}, Metric: lbls}
if r.compareOp() == ValueIsAbove {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: avg}, Metric: lbls}
if r.compareOp() == ruletypes.ValueIsAbove {
if avg > r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueIsBelow {
} else if r.compareOp() == ruletypes.ValueIsBelow {
if avg < r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueIsEq {
} else if r.compareOp() == ruletypes.ValueIsEq {
if avg == r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueIsNotEq {
} else if r.compareOp() == ruletypes.ValueIsNotEq {
if avg != r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueOutsideBounds {
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
if math.Abs(avg) >= r.targetVal() {
shouldAlert = true
}
}
case InTotal:
case ruletypes.InTotal:
// If the sum of all samples matches the condition, the rule is firing.
var sum float64
@@ -536,45 +537,45 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (Sample, bool) {
}
sum += smpl.Value
}
alertSmpl = Sample{Point: Point{V: sum}, Metric: lbls}
if r.compareOp() == ValueIsAbove {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: sum}, Metric: lbls}
if r.compareOp() == ruletypes.ValueIsAbove {
if sum > r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueIsBelow {
} else if r.compareOp() == ruletypes.ValueIsBelow {
if sum < r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueIsEq {
} else if r.compareOp() == ruletypes.ValueIsEq {
if sum == r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueIsNotEq {
} else if r.compareOp() == ruletypes.ValueIsNotEq {
if sum != r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueOutsideBounds {
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
if math.Abs(sum) >= r.targetVal() {
shouldAlert = true
}
}
case Last:
case ruletypes.Last:
// If the last sample matches the condition, the rule is firing.
shouldAlert = false
alertSmpl = Sample{Point: Point{V: series.Points[len(series.Points)-1].Value}, Metric: lbls}
if r.compareOp() == ValueIsAbove {
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: series.Points[len(series.Points)-1].Value}, Metric: lbls}
if r.compareOp() == ruletypes.ValueIsAbove {
if series.Points[len(series.Points)-1].Value > r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueIsBelow {
} else if r.compareOp() == ruletypes.ValueIsBelow {
if series.Points[len(series.Points)-1].Value < r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueIsEq {
} else if r.compareOp() == ruletypes.ValueIsEq {
if series.Points[len(series.Points)-1].Value == r.targetVal() {
shouldAlert = true
}
} else if r.compareOp() == ValueIsNotEq {
} else if r.compareOp() == ruletypes.ValueIsNotEq {
if series.Points[len(series.Points)-1].Value != r.targetVal() {
shouldAlert = true
}

View File

@@ -4,6 +4,7 @@ import (
"testing"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
)
func TestBaseRule_RequireMinPoints(t *testing.T) {
@@ -17,7 +18,7 @@ func TestBaseRule_RequireMinPoints(t *testing.T) {
{
name: "test should skip if less than min points",
rule: &BaseRule{
ruleCondition: &RuleCondition{
ruleCondition: &ruletypes.RuleCondition{
RequireMinPoints: true,
RequiredNumPoints: 4,
},
@@ -33,11 +34,11 @@ func TestBaseRule_RequireMinPoints(t *testing.T) {
{
name: "test should alert if more than min points",
rule: &BaseRule{
ruleCondition: &RuleCondition{
ruleCondition: &ruletypes.RuleCondition{
RequireMinPoints: true,
RequiredNumPoints: 4,
CompareOp: ValueIsAbove,
MatchType: AtleastOnce,
CompareOp: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
Target: &threshold,
},
},

View File

@@ -1,387 +0,0 @@
package rules
import (
"context"
"encoding/json"
"fmt"
"slices"
"strconv"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/uptrace/bun"
"go.uber.org/zap"
)
// Data store to capture user alert rule settings
type RuleDB interface {
// CreateRule stores rule in the db and returns tx and group name (on success)
CreateRule(context.Context, *StoredRule, func(context.Context, int64) error) (int64, error)
// EditRuleTx updates the given rule in the db and returns tx and group name (on success)
EditRule(context.Context, *StoredRule, func(context.Context) error) error
// DeleteRuleTx deletes the given rule in the db and returns tx and group name (on success)
DeleteRule(context.Context, string, func(context.Context) error) error
// GetStoredRules fetches the rule definitions from db
GetStoredRules(ctx context.Context) ([]StoredRule, error)
// GetStoredRule for a given ID from DB
GetStoredRule(ctx context.Context, id string) (*StoredRule, error)
// CreatePlannedMaintenance stores a given maintenance in db
CreatePlannedMaintenance(ctx context.Context, maintenance PlannedMaintenance) (int64, error)
// DeletePlannedMaintenance deletes the given maintenance in the db
DeletePlannedMaintenance(ctx context.Context, id string) (string, error)
// GetPlannedMaintenanceByID fetches the maintenance definition from db by id
GetPlannedMaintenanceByID(ctx context.Context, id string) (*PlannedMaintenance, error)
// EditPlannedMaintenance updates the given maintenance in the db
EditPlannedMaintenance(ctx context.Context, maintenance PlannedMaintenance, id string) (string, error)
// GetAllPlannedMaintenance fetches the maintenance definitions from db
GetAllPlannedMaintenance(ctx context.Context) ([]PlannedMaintenance, error)
// used for internal telemetry
GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error)
}
type StoredRule struct {
bun.BaseModel `bun:"rules"`
Id int `json:"id" db:"id" bun:"id,pk,autoincrement"`
CreatedAt *time.Time `json:"created_at" db:"created_at" bun:"created_at"`
CreatedBy *string `json:"created_by" db:"created_by" bun:"created_by"`
UpdatedAt *time.Time `json:"updated_at" db:"updated_at" bun:"updated_at"`
UpdatedBy *string `json:"updated_by" db:"updated_by" bun:"updated_by"`
Data string `json:"data" db:"data" bun:"data"`
}
type ruleDB struct {
*sqlx.DB
sqlstore sqlstore.SQLStore
}
func NewRuleDB(db *sqlx.DB, sqlstore sqlstore.SQLStore) RuleDB {
return &ruleDB{db, sqlstore}
}
// CreateRule stores a given rule in db and returns task name and error (if any)
func (r *ruleDB) CreateRule(ctx context.Context, storedRule *StoredRule, cb func(context.Context, int64) error) (int64, error) {
err := r.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
_, err := r.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(storedRule).
Exec(ctx)
if err != nil {
return err
}
return cb(ctx, int64(storedRule.Id))
})
if err != nil {
return 0, err
}
return int64(storedRule.Id), nil
}
// EditRule stores a given rule string in database and returns task name and error (if any)
func (r *ruleDB) EditRule(ctx context.Context, storedRule *StoredRule, cb func(context.Context) error) error {
return r.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
_, err := r.sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(storedRule).
WherePK().
Exec(ctx)
if err != nil {
return err
}
return cb(ctx)
})
}
// DeleteRule deletes a given rule with id and returns taskname and error (if any)
func (r *ruleDB) DeleteRule(ctx context.Context, id string, cb func(context.Context) error) error {
if err := r.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
_, err := r.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(&StoredRule{}).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return err
}
return cb(ctx)
}); err != nil {
return err
}
return nil
}
func (r *ruleDB) GetStoredRules(ctx context.Context) ([]StoredRule, error) {
rules := []StoredRule{}
query := "SELECT id, created_at, created_by, updated_at, updated_by, data FROM rules"
err := r.Select(&rules, query)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, err
}
return rules, nil
}
func (r *ruleDB) GetStoredRule(ctx context.Context, id string) (*StoredRule, error) {
intId, err := strconv.Atoi(id)
if err != nil {
return nil, fmt.Errorf("invalid id parameter")
}
rule := &StoredRule{}
query := fmt.Sprintf("SELECT id, created_at, created_by, updated_at, updated_by, data FROM rules WHERE id=%d", intId)
err = r.Get(rule, query)
// zap.L().Info(query)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, err
}
return rule, nil
}
func (r *ruleDB) GetAllPlannedMaintenance(ctx context.Context) ([]PlannedMaintenance, error) {
maintenances := []PlannedMaintenance{}
query := "SELECT id, name, description, schedule, alert_ids, created_at, created_by, updated_at, updated_by FROM planned_maintenance"
err := r.Select(&maintenances, query)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, err
}
return maintenances, nil
}
func (r *ruleDB) GetPlannedMaintenanceByID(ctx context.Context, id string) (*PlannedMaintenance, error) {
maintenance := &PlannedMaintenance{}
query := "SELECT id, name, description, schedule, alert_ids, created_at, created_by, updated_at, updated_by FROM planned_maintenance WHERE id=$1"
err := r.Get(maintenance, query, id)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, err
}
return maintenance, nil
}
func (r *ruleDB) CreatePlannedMaintenance(ctx context.Context, maintenance PlannedMaintenance) (int64, error) {
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return 0, errors.New("no claims found in context")
}
maintenance.CreatedBy = claims.Email
maintenance.CreatedAt = time.Now()
maintenance.UpdatedBy = claims.Email
maintenance.UpdatedAt = time.Now()
query := "INSERT INTO planned_maintenance (name, description, schedule, alert_ids, created_at, created_by, updated_at, updated_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"
result, err := r.Exec(query, maintenance.Name, maintenance.Description, maintenance.Schedule, maintenance.AlertIds, maintenance.CreatedAt, maintenance.CreatedBy, maintenance.UpdatedAt, maintenance.UpdatedBy)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return 0, err
}
return result.LastInsertId()
}
func (r *ruleDB) DeletePlannedMaintenance(ctx context.Context, id string) (string, error) {
query := "DELETE FROM planned_maintenance WHERE id=$1"
_, err := r.Exec(query, id)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return "", err
}
return "", nil
}
func (r *ruleDB) EditPlannedMaintenance(ctx context.Context, maintenance PlannedMaintenance, id string) (string, error) {
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return "", errors.New("no claims found in context")
}
maintenance.UpdatedBy = claims.Email
maintenance.UpdatedAt = time.Now()
query := "UPDATE planned_maintenance SET name=$1, description=$2, schedule=$3, alert_ids=$4, updated_at=$5, updated_by=$6 WHERE id=$7"
_, err := r.Exec(query, maintenance.Name, maintenance.Description, maintenance.Schedule, maintenance.AlertIds, maintenance.UpdatedAt, maintenance.UpdatedBy, id)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return "", err
}
return "", nil
}
func (r *ruleDB) getChannels() (*[]model.ChannelItem, *model.ApiError) {
channels := []model.ChannelItem{}
query := "SELECT id, created_at, updated_at, name, type, data data FROM notification_channels"
err := r.Select(&channels, query)
zap.L().Info(query)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
return &channels, nil
}
func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
alertsInfo := model.AlertsInfo{}
// fetch alerts from rules db
query := "SELECT data FROM rules"
var alertsData []string
var alertNames []string
err := r.Select(&alertsData, query)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return &alertsInfo, err
}
for _, alert := range alertsData {
var rule GettableRule
if strings.Contains(alert, "time_series_v2") {
alertsInfo.AlertsWithTSV2 = alertsInfo.AlertsWithTSV2 + 1
}
err = json.Unmarshal([]byte(alert), &rule)
if err != nil {
zap.L().Error("invalid rule data", zap.Error(err))
continue
}
alertNames = append(alertNames, rule.AlertName)
if rule.AlertType == AlertTypeLogs {
alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1
if rule.RuleCondition != nil && rule.RuleCondition.CompositeQuery != nil {
if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeClickHouseSQL {
if strings.Contains(alert, "signoz_logs.distributed_logs") ||
strings.Contains(alert, "signoz_logs.logs") {
alertsInfo.AlertsWithLogsChQuery = alertsInfo.AlertsWithLogsChQuery + 1
}
}
}
for _, query := range rule.RuleCondition.CompositeQuery.BuilderQueries {
if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeBuilder {
if query.Filters != nil {
for _, item := range query.Filters.Items {
if slices.Contains([]string{"contains", "ncontains", "like", "nlike"}, string(item.Operator)) {
if item.Key.Key != "body" {
alertsInfo.AlertsWithLogsContainsOp += 1
}
}
}
}
}
}
} else if rule.AlertType == AlertTypeMetric {
alertsInfo.MetricBasedAlerts = alertsInfo.MetricBasedAlerts + 1
if rule.RuleCondition != nil && rule.RuleCondition.CompositeQuery != nil {
if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeBuilder {
alertsInfo.MetricsBuilderQueries = alertsInfo.MetricsBuilderQueries + 1
} else if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeClickHouseSQL {
alertsInfo.MetricsClickHouseQueries = alertsInfo.MetricsClickHouseQueries + 1
} else if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypePromQL {
alertsInfo.MetricsPrometheusQueries = alertsInfo.MetricsPrometheusQueries + 1
for _, query := range rule.RuleCondition.CompositeQuery.PromQueries {
if strings.Contains(query.Query, "signoz_") {
alertsInfo.SpanMetricsPrometheusQueries = alertsInfo.SpanMetricsPrometheusQueries + 1
}
}
}
}
if rule.RuleType == RuleTypeAnomaly {
alertsInfo.AnomalyBasedAlerts = alertsInfo.AnomalyBasedAlerts + 1
}
} else if rule.AlertType == AlertTypeTraces {
alertsInfo.TracesBasedAlerts = alertsInfo.TracesBasedAlerts + 1
if rule.RuleCondition != nil && rule.RuleCondition.CompositeQuery != nil {
if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeClickHouseSQL {
if strings.Contains(alert, "signoz_traces.distributed_signoz_index_v2") ||
strings.Contains(alert, "signoz_traces.distributed_signoz_spans") ||
strings.Contains(alert, "signoz_traces.distributed_signoz_error_index_v2") {
alertsInfo.AlertsWithTraceChQuery = alertsInfo.AlertsWithTraceChQuery + 1
}
}
}
}
alertsInfo.TotalAlerts = alertsInfo.TotalAlerts + 1
if !rule.PostableRule.Disabled {
alertsInfo.TotalActiveAlerts = alertsInfo.TotalActiveAlerts + 1
}
}
alertsInfo.AlertNames = alertNames
channels, _ := r.getChannels()
if channels != nil {
alertsInfo.TotalChannels = len(*channels)
for _, channel := range *channels {
if channel.Type == "slack" {
alertsInfo.SlackChannels = alertsInfo.SlackChannels + 1
}
if channel.Type == "webhook" {
alertsInfo.WebHookChannels = alertsInfo.WebHookChannels + 1
}
if channel.Type == "email" {
alertsInfo.EmailChannels = alertsInfo.EmailChannels + 1
}
if channel.Type == "pagerduty" {
alertsInfo.PagerDutyChannels = alertsInfo.PagerDutyChannels + 1
}
if channel.Type == "opsgenie" {
alertsInfo.OpsGenieChannels = alertsInfo.OpsGenieChannels + 1
}
if channel.Type == "msteams" {
alertsInfo.MSTeamsChannels = alertsInfo.MSTeamsChannels + 1
}
}
}
return &alertsInfo, nil
}

View File

@@ -23,16 +23,21 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type PrepareTaskOptions struct {
Rule *PostableRule
Rule *ruletypes.PostableRule
TaskName string
RuleDB RuleDB
RuleStore ruletypes.RuleStore
MaintenanceStore ruletypes.MaintenanceStore
Logger *zap.Logger
Reader interfaces.Reader
Cache cache.Cache
@@ -41,11 +46,13 @@ type PrepareTaskOptions struct {
SQLStore sqlstore.SQLStore
UseLogsNewSchema bool
UseTraceNewSchema bool
OrgID string
}
type PrepareTestRuleOptions struct {
Rule *PostableRule
RuleDB RuleDB
Rule *ruletypes.PostableRule
RuleStore ruletypes.RuleStore
MaintenanceStore ruletypes.MaintenanceStore
Logger *zap.Logger
Reader interfaces.Reader
Cache cache.Cache
@@ -109,7 +116,8 @@ type Manager struct {
mtx sync.RWMutex
block chan struct{}
// datastore to store alert definitions
ruleDB RuleDB
ruleStore ruletypes.RuleStore
maintenanceStore ruletypes.MaintenanceStore
logger *zap.Logger
reader interfaces.Reader
@@ -146,7 +154,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
var task Task
ruleId := RuleIdFromTaskName(opts.TaskName)
if opts.Rule.RuleType == RuleTypeThreshold {
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := NewThresholdRule(
ruleId,
@@ -165,9 +173,9 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
rules = append(rules, tr)
// create ch rule task for evalution
task = newTask(TaskTypeCh, opts.TaskName, taskNamesuffix, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
task = newTask(TaskTypeCh, opts.TaskName, taskNamesuffix, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == RuleTypeProm {
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
// create promql rule
pr, err := NewPromRule(
@@ -186,10 +194,10 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
rules = append(rules, pr)
// create promql rule task for evalution
task = newTask(TaskTypeProm, opts.TaskName, taskNamesuffix, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
task = newTask(TaskTypeProm, opts.TaskName, taskNamesuffix, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else {
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, RuleTypeProm, RuleTypeThreshold)
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
}
return task, nil
@@ -199,12 +207,15 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
// by calling the Run method.
func NewManager(o *ManagerOptions) (*Manager, error) {
o = defaultOptions(o)
db := NewRuleDB(o.DBConn, o.SQLStore)
telemetry.GetInstance().SetAlertsInfoCallback(db.GetAlertsInfo)
ruleStore := sqlrulestore.NewRuleStore(o.DBConn, o.SQLStore)
maintenanceStore := sqlrulestore.NewMaintenanceStore(o.SQLStore)
telemetry.GetInstance().SetAlertsInfoCallback(ruleStore.GetAlertsInfo)
m := &Manager{
tasks: map[string]Task{},
rules: map[string]Rule{},
ruleDB: db,
ruleStore: ruleStore,
maintenanceStore: maintenanceStore,
opts: o,
block: make(chan struct{}),
logger: o.Logger,
@@ -219,15 +230,19 @@ func NewManager(o *ManagerOptions) (*Manager, error) {
return m, nil
}
func (m *Manager) Start() {
if err := m.initiate(); err != nil {
func (m *Manager) Start(ctx context.Context) {
if err := m.initiate(ctx); err != nil {
zap.L().Error("failed to initialize alerting rules manager", zap.Error(err))
}
m.run()
m.run(ctx)
}
func (m *Manager) RuleDB() RuleDB {
return m.ruleDB
func (m *Manager) RuleStore() ruletypes.RuleStore {
return m.ruleStore
}
func (m *Manager) MaintenanceStore() ruletypes.MaintenanceStore {
return m.maintenanceStore
}
func (m *Manager) Pause(b bool) {
@@ -238,44 +253,51 @@ func (m *Manager) Pause(b bool) {
}
}
func (m *Manager) initiate() error {
storedRules, err := m.ruleDB.GetStoredRules(context.Background())
func (m *Manager) initiate(ctx context.Context) error {
orgIDs, err := m.ruleStore.ListOrgs(ctx)
if err != nil {
return err
}
if len(storedRules) == 0 {
return nil
}
var loadErrors []error
for _, rec := range storedRules {
taskName := fmt.Sprintf("%d-groupname", rec.Id)
parsedRule, err := ParsePostableRule([]byte(rec.Data))
for _, orgID := range orgIDs {
storedRules, err := m.ruleStore.GetStoredRules(ctx, orgID)
if err != nil {
if errors.Is(err, ErrFailedToParseJSON) {
zap.L().Info("failed to load rule in json format, trying yaml now:", zap.String("name", taskName))
return err
}
if len(storedRules) == 0 {
return nil
}
// see if rule is stored in yaml format
parsedRule, err = parsePostableRule([]byte(rec.Data), RuleDataKindYaml)
for _, rec := range storedRules {
taskName := fmt.Sprintf("%s-groupname", rec.ID.StringValue())
parsedRule, err := ruletypes.ParsePostableRule([]byte(rec.Data))
if err != nil {
zap.L().Error("failed to parse and initialize yaml rule", zap.String("name", taskName), zap.Error(err))
if err != nil {
if errors.Is(err, ruletypes.ErrFailedToParseJSON) {
zap.L().Info("failed to load rule in json format, trying yaml now:", zap.String("name", taskName))
// see if rule is stored in yaml format
parsedRule, err = ruletypes.ParsePostableRuleWithKind([]byte(rec.Data), ruletypes.RuleDataKindYaml)
if err != nil {
zap.L().Error("failed to parse and initialize yaml rule", zap.String("name", taskName), zap.Error(err))
// just one rule is being parsed so expect just one error
loadErrors = append(loadErrors, err)
continue
}
} else {
zap.L().Error("failed to parse and initialize rule", zap.String("name", taskName), zap.Error(err))
// just one rule is being parsed so expect just one error
loadErrors = append(loadErrors, err)
continue
}
} else {
zap.L().Error("failed to parse and initialize rule", zap.String("name", taskName), zap.Error(err))
// just one rule is being parsed so expect just one error
loadErrors = append(loadErrors, err)
continue
}
}
if !parsedRule.Disabled {
err := m.addTask(parsedRule, taskName)
if err != nil {
zap.L().Error("failed to load the rule definition", zap.String("name", taskName), zap.Error(err))
if !parsedRule.Disabled {
err := m.addTask(ctx, orgID, parsedRule, taskName)
if err != nil {
zap.L().Error("failed to load the rule definition", zap.String("name", taskName), zap.Error(err))
}
}
}
}
@@ -288,13 +310,13 @@ func (m *Manager) initiate() error {
}
// Run starts processing of the rule manager.
func (m *Manager) run() {
func (m *Manager) run(_ context.Context) {
// initiate blocked tasks
close(m.block)
}
// Stop the rule manager's rule evaluation cycles.
func (m *Manager) Stop() {
func (m *Manager) Stop(ctx context.Context) {
m.mtx.Lock()
defer m.mtx.Unlock()
@@ -309,28 +331,41 @@ func (m *Manager) Stop() {
// EditRuleDefinition writes the rule definition to the
// datastore and also updates the rule executor
func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error {
func (m *Manager) EditRule(ctx context.Context, ruleStr string, idStr string) error {
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return errors.New("claims not found in context")
}
parsedRule, err := ParsePostableRule([]byte(ruleStr))
ruleUUID, err := valuer.NewUUID(idStr)
if err != nil {
id, err := strconv.Atoi(idStr)
if err != nil {
return err
}
ruleHistory, err := m.ruleStore.GetRuleUUID(ctx, id)
if err != nil {
return err
}
ruleUUID = ruleHistory.RuleUUID
}
parsedRule, err := ruletypes.ParsePostableRule([]byte(ruleStr))
if err != nil {
return err
}
existingRule, err := m.ruleDB.GetStoredRule(ctx, id)
existingRule, err := m.ruleStore.GetStoredRule(ctx, ruleUUID)
if err != nil {
return err
}
now := time.Now()
existingRule.UpdatedAt = &now
existingRule.UpdatedBy = &claims.Email
existingRule.UpdatedAt = time.Now()
existingRule.UpdatedBy = claims.Email
existingRule.Data = ruleStr
return m.ruleDB.EditRule(ctx, existingRule, func(ctx context.Context) error {
return m.ruleStore.EditRule(ctx, existingRule, func(ctx context.Context) error {
cfg, err := m.alertmanager.GetConfig(ctx, claims.OrgID)
if err != nil {
return err
@@ -350,7 +385,7 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error
preferredChannels = parsedRule.PreferredChannels
}
err = cfg.UpdateRuleIDMatcher(id, preferredChannels)
err = cfg.UpdateRuleIDMatcher(ruleUUID.StringValue(), preferredChannels)
if err != nil {
return err
}
@@ -361,7 +396,7 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error
}
if !m.opts.DisableRules {
err = m.syncRuleStateWithTask(prepareTaskName(existingRule.Id), parsedRule)
err = m.syncRuleStateWithTask(ctx, claims.OrgID, prepareTaskName(existingRule.ID.StringValue()), parsedRule)
if err != nil {
return err
}
@@ -371,25 +406,26 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error
})
}
func (m *Manager) editTask(rule *PostableRule, taskName string) error {
func (m *Manager) editTask(_ context.Context, orgID string, rule *ruletypes.PostableRule, taskName string) error {
m.mtx.Lock()
defer m.mtx.Unlock()
zap.L().Debug("editing a rule task", zap.String("name", taskName))
newTask, err := m.prepareTaskFunc(PrepareTaskOptions{
Rule: rule,
TaskName: taskName,
RuleDB: m.ruleDB,
Logger: m.logger,
Reader: m.reader,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.prepareNotifyFunc(),
SQLStore: m.sqlstore,
Rule: rule,
TaskName: taskName,
RuleStore: m.ruleStore,
MaintenanceStore: m.maintenanceStore,
Logger: m.logger,
Reader: m.reader,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.prepareNotifyFunc(),
SQLStore: m.sqlstore,
UseLogsNewSchema: m.opts.UseLogsNewSchema,
UseTraceNewSchema: m.opts.UseTraceNewSchema,
OrgID: orgID,
})
if err != nil {
@@ -426,11 +462,11 @@ func (m *Manager) editTask(rule *PostableRule, taskName string) error {
return nil
}
func (m *Manager) DeleteRule(ctx context.Context, id string) error {
_, err := strconv.Atoi(id)
func (m *Manager) DeleteRule(ctx context.Context, idStr string) error {
id, err := valuer.NewUUID(idStr)
if err != nil {
zap.L().Error("delete rule received an rule id in invalid format, must be a number", zap.String("id", id), zap.Error(err))
return fmt.Errorf("delete rule received an rule id in invalid format, must be a number")
zap.L().Error("delete rule received an rule id in invalid format, must be a valid uuid-v7", zap.String("id", idStr), zap.Error(err))
return fmt.Errorf("delete rule received an rule id in invalid format, must be a valid uuid-v7")
}
claims, ok := authtypes.ClaimsFromContext(ctx)
@@ -438,18 +474,18 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error {
return errors.New("claims not found in context")
}
_, err = m.ruleDB.GetStoredRule(ctx, id)
_, err = m.ruleStore.GetStoredRule(ctx, id)
if err != nil {
return err
}
return m.ruleDB.DeleteRule(ctx, id, func(ctx context.Context) error {
return m.ruleStore.DeleteRule(ctx, id, func(ctx context.Context) error {
cfg, err := m.alertmanager.GetConfig(ctx, claims.OrgID)
if err != nil {
return err
}
err = cfg.DeleteRuleIDMatcher(id)
err = cfg.DeleteRuleIDMatcher(id.StringValue())
if err != nil {
return err
}
@@ -459,7 +495,7 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error {
return err
}
taskName := prepareTaskName(id)
taskName := prepareTaskName(id.StringValue())
if !m.opts.DisableRules {
m.deleteTask(taskName)
}
@@ -486,27 +522,35 @@ func (m *Manager) deleteTask(taskName string) {
// CreateRule stores rule def into db and also
// starts an executor for the rule
func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule, error) {
parsedRule, err := ParsePostableRule([]byte(ruleStr))
if err != nil {
return nil, err
}
func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*ruletypes.GettableRule, error) {
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return nil, errors.New("claims not found in context")
}
now := time.Now()
storedRule := &StoredRule{
CreatedAt: &now,
CreatedBy: &claims.Email,
UpdatedAt: &now,
UpdatedBy: &claims.Email,
Data: ruleStr,
parsedRule, err := ruletypes.ParsePostableRule([]byte(ruleStr))
if err != nil {
return nil, err
}
id, err := m.ruleDB.CreateRule(ctx, storedRule, func(ctx context.Context, id int64) error {
now := time.Now()
storedRule := &ruletypes.Rule{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: now,
UpdatedAt: now,
},
UserAuditable: types.UserAuditable{
CreatedBy: claims.Email,
UpdatedBy: claims.Email,
},
Data: ruleStr,
OrgID: claims.OrgID,
}
id, err := m.ruleStore.CreateRule(ctx, storedRule, func(ctx context.Context, id valuer.UUID) error {
cfg, err := m.alertmanager.GetConfig(ctx, claims.OrgID)
if err != nil {
return err
@@ -526,7 +570,7 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule
preferredChannels = parsedRule.PreferredChannels
}
err = cfg.CreateRuleIDMatcher(fmt.Sprintf("%d", id), preferredChannels)
err = cfg.CreateRuleIDMatcher(id.StringValue(), preferredChannels)
if err != nil {
return err
}
@@ -536,9 +580,9 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule
return err
}
taskName := prepareTaskName(id)
taskName := prepareTaskName(id.StringValue())
if !m.opts.DisableRules {
if err := m.addTask(parsedRule, taskName); err != nil {
if err := m.addTask(ctx, claims.OrgID, parsedRule, taskName); err != nil {
return err
}
}
@@ -549,30 +593,31 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule
return nil, err
}
return &GettableRule{
Id: fmt.Sprintf("%d", id),
return &ruletypes.GettableRule{
Id: id.StringValue(),
PostableRule: *parsedRule,
}, nil
}
func (m *Manager) addTask(rule *PostableRule, taskName string) error {
func (m *Manager) addTask(_ context.Context, orgID string, rule *ruletypes.PostableRule, taskName string) error {
m.mtx.Lock()
defer m.mtx.Unlock()
zap.L().Debug("adding a new rule task", zap.String("name", taskName))
newTask, err := m.prepareTaskFunc(PrepareTaskOptions{
Rule: rule,
TaskName: taskName,
RuleDB: m.ruleDB,
Logger: m.logger,
Reader: m.reader,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.prepareNotifyFunc(),
SQLStore: m.sqlstore,
Rule: rule,
TaskName: taskName,
RuleStore: m.ruleStore,
MaintenanceStore: m.maintenanceStore,
Logger: m.logger,
Reader: m.reader,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.prepareNotifyFunc(),
SQLStore: m.sqlstore,
UseLogsNewSchema: m.opts.UseLogsNewSchema,
UseTraceNewSchema: m.opts.UseTraceNewSchema,
OrgID: orgID,
})
if err != nil {
@@ -648,17 +693,17 @@ func (m *Manager) Rules() []Rule {
}
// TriggeredAlerts returns the list of the manager's rules.
func (m *Manager) TriggeredAlerts() []*NamedAlert {
func (m *Manager) TriggeredAlerts() []*ruletypes.NamedAlert {
// m.mtx.RLock()
// defer m.mtx.RUnlock()
namedAlerts := []*NamedAlert{}
namedAlerts := []*ruletypes.NamedAlert{}
for _, r := range m.rules {
active := r.ActiveAlerts()
for _, a := range active {
awn := &NamedAlert{
awn := &ruletypes.NamedAlert{
Alert: a,
Name: r.Name(),
}
@@ -670,11 +715,11 @@ func (m *Manager) TriggeredAlerts() []*NamedAlert {
}
// NotifyFunc sends notifications about a set of alerts generated by the given expression.
type NotifyFunc func(ctx context.Context, orgID string, expr string, alerts ...*Alert)
type NotifyFunc func(ctx context.Context, orgID string, expr string, alerts ...*ruletypes.Alert)
// prepareNotifyFunc implements the NotifyFunc for a Notifier.
func (m *Manager) prepareNotifyFunc() NotifyFunc {
return func(ctx context.Context, orgID string, expr string, alerts ...*Alert) {
return func(ctx context.Context, orgID string, expr string, alerts ...*ruletypes.Alert) {
var res []*alertmanagertypes.PostableAlert
for _, alert := range alerts {
@@ -707,7 +752,7 @@ func (m *Manager) prepareNotifyFunc() NotifyFunc {
}
func (m *Manager) prepareTestNotifyFunc() NotifyFunc {
return func(ctx context.Context, orgID string, expr string, alerts ...*Alert) {
return func(ctx context.Context, orgID string, expr string, alerts ...*ruletypes.Alert) {
if len(alerts) == 0 {
return
}
@@ -758,26 +803,29 @@ func (m *Manager) ListActiveRules() ([]Rule, error) {
return ruleList, nil
}
func (m *Manager) ListRuleStates(ctx context.Context) (*GettableRules, error) {
func (m *Manager) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules, error) {
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return nil, errors.New("claims not found in context")
}
// fetch rules from DB
storedRules, err := m.ruleDB.GetStoredRules(ctx)
storedRules, err := m.ruleStore.GetStoredRules(ctx, claims.OrgID)
if err != nil {
return nil, err
}
// initiate response object
resp := make([]*GettableRule, 0)
resp := make([]*ruletypes.GettableRule, 0)
for _, s := range storedRules {
ruleResponse := &GettableRule{}
ruleResponse := &ruletypes.GettableRule{}
if err := json.Unmarshal([]byte(s.Data), ruleResponse); err != nil { // Parse []byte to go struct pointer
zap.L().Error("failed to unmarshal rule from db", zap.Int("id", s.Id), zap.Error(err))
zap.L().Error("failed to unmarshal rule from db", zap.String("id", s.ID.StringValue()), zap.Error(err))
continue
}
ruleResponse.Id = fmt.Sprintf("%d", s.Id)
ruleResponse.Id = s.ID.StringValue()
// fetch state of rule from memory
if rm, ok := m.rules[ruleResponse.Id]; !ok {
@@ -786,26 +834,40 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*GettableRules, error) {
} else {
ruleResponse.State = rm.State()
}
ruleResponse.CreatedAt = s.CreatedAt
ruleResponse.CreatedBy = s.CreatedBy
ruleResponse.UpdatedAt = s.UpdatedAt
ruleResponse.UpdatedBy = s.UpdatedBy
ruleResponse.CreatedAt = &s.CreatedAt
ruleResponse.CreatedBy = &s.CreatedBy
ruleResponse.UpdatedAt = &s.UpdatedAt
ruleResponse.UpdatedBy = &s.UpdatedBy
resp = append(resp, ruleResponse)
}
return &GettableRules{Rules: resp}, nil
return &ruletypes.GettableRules{Rules: resp}, nil
}
func (m *Manager) GetRule(ctx context.Context, id string) (*GettableRule, error) {
s, err := m.ruleDB.GetStoredRule(ctx, id)
func (m *Manager) GetRule(ctx context.Context, idStr string) (*ruletypes.GettableRule, error) {
ruleUUID, err := valuer.NewUUID(idStr)
if err != nil {
id, err := strconv.Atoi(idStr)
if err != nil {
return nil, err
}
ruleHistory, err := m.ruleStore.GetRuleUUID(ctx, id)
if err != nil {
return nil, err
}
ruleUUID = ruleHistory.RuleUUID
}
s, err := m.ruleStore.GetStoredRule(ctx, ruleUUID)
if err != nil {
return nil, err
}
r := &GettableRule{}
r := &ruletypes.GettableRule{}
if err := json.Unmarshal([]byte(s.Data), r); err != nil {
return nil, err
}
r.Id = fmt.Sprintf("%d", s.Id)
r.Id = ruleUUID.StringValue()
// fetch state of rule from memory
if rm, ok := m.rules[r.Id]; !ok {
r.State = model.StateDisabled
@@ -813,10 +875,10 @@ func (m *Manager) GetRule(ctx context.Context, id string) (*GettableRule, error)
} else {
r.State = rm.State()
}
r.CreatedAt = s.CreatedAt
r.CreatedBy = s.CreatedBy
r.UpdatedAt = s.UpdatedAt
r.UpdatedBy = s.UpdatedBy
r.CreatedAt = &s.CreatedAt
r.CreatedBy = &s.CreatedBy
r.UpdatedAt = &s.UpdatedAt
r.UpdatedBy = &s.UpdatedBy
return r, nil
}
@@ -824,7 +886,7 @@ func (m *Manager) GetRule(ctx context.Context, id string) (*GettableRule, error)
// syncRuleStateWithTask ensures that the state of a stored rule matches
// the task state. For example - if a stored rule is disabled, then
// there is no task running against it.
func (m *Manager) syncRuleStateWithTask(taskName string, rule *PostableRule) error {
func (m *Manager) syncRuleStateWithTask(ctx context.Context, orgID string, taskName string, rule *ruletypes.PostableRule) error {
if rule.Disabled {
// check if rule has any task running
@@ -836,11 +898,11 @@ func (m *Manager) syncRuleStateWithTask(taskName string, rule *PostableRule) err
// check if rule has a task running
if _, ok := m.tasks[taskName]; !ok {
// rule has not task, start one
if err := m.addTask(rule, taskName); err != nil {
if err := m.addTask(ctx, orgID, rule, taskName); err != nil {
return err
}
} else {
if err := m.editTask(rule, taskName); err != nil {
if err := m.editTask(ctx, orgID, rule, taskName); err != nil {
return err
}
}
@@ -855,40 +917,41 @@ func (m *Manager) syncRuleStateWithTask(taskName string, rule *PostableRule) err
// - over write the patch attributes received in input (ruleStr)
// - re-deploy or undeploy task as necessary
// - update the patched rule in the DB
func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleId string) (*GettableRule, error) {
func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleIdStr string) (*ruletypes.GettableRule, error) {
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return nil, errors.New("claims not found in context")
}
if ruleId == "" {
return nil, fmt.Errorf("id is mandatory for patching rule")
ruleID, err := valuer.NewUUID(ruleIdStr)
if err != nil {
return nil, errors.New(err.Error())
}
taskName := prepareTaskName(ruleId)
taskName := prepareTaskName(ruleID.StringValue())
// retrieve rule from DB
storedJSON, err := m.ruleDB.GetStoredRule(ctx, ruleId)
storedJSON, err := m.ruleStore.GetStoredRule(ctx, ruleID)
if err != nil {
zap.L().Error("failed to get stored rule with given id", zap.String("id", ruleId), zap.Error(err))
zap.L().Error("failed to get stored rule with given id", zap.String("id", ruleID.StringValue()), zap.Error(err))
return nil, err
}
// storedRule holds the current stored rule from DB
storedRule := PostableRule{}
storedRule := ruletypes.PostableRule{}
if err := json.Unmarshal([]byte(storedJSON.Data), &storedRule); err != nil {
zap.L().Error("failed to unmarshal stored rule with given id", zap.String("id", ruleId), zap.Error(err))
zap.L().Error("failed to unmarshal stored rule with given id", zap.String("id", ruleID.StringValue()), zap.Error(err))
return nil, err
}
// patchedRule is combo of stored rule and patch received in the request
patchedRule, err := parseIntoRule(storedRule, []byte(ruleStr), "json")
patchedRule, err := ruletypes.ParseIntoRule(storedRule, []byte(ruleStr), "json")
if err != nil {
return nil, err
}
// deploy or un-deploy task according to patched (new) rule state
if err := m.syncRuleStateWithTask(taskName, patchedRule); err != nil {
if err := m.syncRuleStateWithTask(ctx, claims.OrgID, taskName, patchedRule); err != nil {
zap.L().Error("failed to sync stored rule state with the task", zap.String("taskName", taskName), zap.Error(err))
return nil, err
}
@@ -901,25 +964,25 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleId string)
now := time.Now()
storedJSON.Data = string(patchedRuleBytes)
storedJSON.UpdatedBy = &claims.Email
storedJSON.UpdatedAt = &now
storedJSON.UpdatedBy = claims.Email
storedJSON.UpdatedAt = now
err = m.ruleDB.EditRule(ctx, storedJSON, func(ctx context.Context) error { return nil })
err = m.ruleStore.EditRule(ctx, storedJSON, func(ctx context.Context) error { return nil })
if err != nil {
if err := m.syncRuleStateWithTask(taskName, &storedRule); err != nil {
if err := m.syncRuleStateWithTask(ctx, claims.OrgID, taskName, &storedRule); err != nil {
zap.L().Error("failed to restore rule after patch failure", zap.String("taskName", taskName), zap.Error(err))
}
return nil, err
}
// prepare http response
response := GettableRule{
Id: ruleId,
response := ruletypes.GettableRule{
Id: ruleID.StringValue(),
PostableRule: *patchedRule,
}
// fetch state of rule from memory
if rm, ok := m.rules[ruleId]; !ok {
if rm, ok := m.rules[ruleID.StringValue()]; !ok {
response.State = model.StateDisabled
response.Disabled = true
} else {
@@ -933,7 +996,7 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleId string)
// sends a test notification. returns alert count and error (if any)
func (m *Manager) TestNotification(ctx context.Context, ruleStr string) (int, *model.ApiError) {
parsedRule, err := ParsePostableRule([]byte(ruleStr))
parsedRule, err := ruletypes.ParsePostableRule([]byte(ruleStr))
if err != nil {
return 0, model.BadRequest(err)
@@ -941,7 +1004,8 @@ func (m *Manager) TestNotification(ctx context.Context, ruleStr string) (int, *m
alertCount, apiErr := m.prepareTestRuleFunc(PrepareTestRuleOptions{
Rule: parsedRule,
RuleDB: m.ruleDB,
RuleStore: m.ruleStore,
MaintenanceStore: m.maintenanceStore,
Logger: m.logger,
Reader: m.reader,
Cache: m.cache,
@@ -955,33 +1019,37 @@ func (m *Manager) TestNotification(ctx context.Context, ruleStr string) (int, *m
return alertCount, apiErr
}
func (m *Manager) GetAlertDetailsForMetricNames(ctx context.Context, metricNames []string) (map[string][]GettableRule, *model.ApiError) {
result := make(map[string][]GettableRule)
func (m *Manager) GetAlertDetailsForMetricNames(ctx context.Context, metricNames []string) (map[string][]ruletypes.GettableRule, *model.ApiError) {
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return nil, &model.ApiError{Typ: model.ErrorExec, Err: errors.New("claims not found in context")}
}
rules, err := m.ruleDB.GetStoredRules(ctx)
result := make(map[string][]ruletypes.GettableRule)
rules, err := m.ruleStore.GetStoredRules(ctx, claims.OrgID)
if err != nil {
zap.L().Error("Error getting stored rules", zap.Error(err))
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
}
metricRulesMap := make(map[string][]GettableRule)
metricRulesMap := make(map[string][]ruletypes.GettableRule)
for _, storedRule := range rules {
var rule GettableRule
var rule ruletypes.GettableRule
if err := json.Unmarshal([]byte(storedRule.Data), &rule); err != nil {
zap.L().Error("Invalid rule data", zap.Error(err))
continue
}
if rule.AlertType != AlertTypeMetric || rule.RuleCondition == nil || rule.RuleCondition.CompositeQuery == nil {
if rule.AlertType != ruletypes.AlertTypeMetric || rule.RuleCondition == nil || rule.RuleCondition.CompositeQuery == nil {
continue
}
rule.Id = fmt.Sprintf("%d", storedRule.Id)
rule.CreatedAt = storedRule.CreatedAt
rule.CreatedBy = storedRule.CreatedBy
rule.UpdatedAt = storedRule.UpdatedAt
rule.UpdatedBy = storedRule.UpdatedBy
rule.Id = storedRule.ID.StringValue()
rule.CreatedAt = &storedRule.CreatedAt
rule.CreatedBy = &storedRule.CreatedBy
rule.UpdatedAt = &storedRule.UpdatedAt
rule.UpdatedBy = &storedRule.UpdatedBy
for _, query := range rule.RuleCondition.CompositeQuery.BuilderQueries {
if query.AggregateAttribute.Key != "" {
@@ -1013,7 +1081,7 @@ func (m *Manager) GetAlertDetailsForMetricNames(ctx context.Context, metricNames
for _, metricName := range metricNames {
if rules, exists := metricRulesMap[metricName]; exists {
seen := make(map[string]bool)
uniqueRules := make([]GettableRule, 0)
uniqueRules := make([]ruletypes.GettableRule, 0)
for _, rule := range rules {
if !seen[rule.Id] {

View File

@@ -16,6 +16,7 @@ import (
qslabels "github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/prometheus/prometheus/promql"
yaml "gopkg.in/yaml.v2"
)
@@ -27,7 +28,7 @@ type PromRule struct {
func NewPromRule(
id string,
postableRule *PostableRule,
postableRule *ruletypes.PostableRule,
logger *zap.Logger,
reader interfaces.Reader,
prometheus prometheus.Prometheus,
@@ -55,8 +56,8 @@ func NewPromRule(
return &p, nil
}
func (r *PromRule) Type() RuleType {
return RuleTypeProm
func (r *PromRule) Type() ruletypes.RuleType {
return ruletypes.RuleTypeProm
}
func (r *PromRule) GetSelectedQuery() string {
@@ -110,7 +111,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
zap.L().Info("evaluating promql query", zap.String("name", r.Name()), zap.String("query", q))
res, err := r.RunAlertQuery(ctx, q, start, end, interval)
if err != nil {
r.SetHealth(HealthBad)
r.SetHealth(ruletypes.HealthBad)
r.SetLastError(err)
return nil, err
}
@@ -120,7 +121,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
resultFPs := map[uint64]struct{}{}
var alerts = make(map[uint64]*Alert, len(res))
var alerts = make(map[uint64]*ruletypes.Alert, len(res))
for _, series := range res {
l := make(map[string]string, len(series.Metric))
@@ -140,14 +141,14 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
threshold := valueFormatter.Format(r.targetVal(), r.Unit())
tmplData := AlertTemplateData(l, valueFormatter.Format(alertSmpl.V, r.Unit()), threshold)
tmplData := ruletypes.AlertTemplateData(l, valueFormatter.Format(alertSmpl.V, r.Unit()), threshold)
// Inject some convenience variables that are easier to remember for users
// who are not used to Go's templating system.
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
expand := func(text string) string {
tmpl := NewTemplateExpander(
tmpl := ruletypes.NewTemplateExpander(
ctx,
defs+text,
"__alert_"+r.Name(),
@@ -187,12 +188,12 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
err = fmt.Errorf("vector contains metrics with the same labelset after applying alert labels")
// We have already acquired the lock above hence using SetHealth and
// SetLastError will deadlock.
r.health = HealthBad
r.health = ruletypes.HealthBad
r.lastError = err
return nil, err
}
alerts[h] = &Alert{
alerts[h] = &ruletypes.Alert{
Labels: lbs,
QueryResultLables: resultLabels,
Annotations: annotations,
@@ -231,7 +232,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
if _, ok := resultFPs[fp]; !ok {
// If the alert was previously firing, keep it around for a given
// retention time so it is reported as resolved to the AlertManager.
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ResolvedRetention) {
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
delete(r.Active, fp)
}
if a.State != model.StateInactive {
@@ -270,7 +271,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
}
}
r.health = HealthGood
r.health = ruletypes.HealthGood
r.lastError = err
currentState := r.State()
@@ -289,10 +290,10 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error)
func (r *PromRule) String() string {
ar := PostableRule{
ar := ruletypes.PostableRule{
AlertName: r.name,
RuleCondition: r.ruleCondition,
EvalWindow: Duration(r.evalWindow),
EvalWindow: ruletypes.Duration(r.evalWindow),
Labels: r.labels.Map(),
Annotations: r.annotations.Map(),
PreferredChannels: r.preferredChannels,

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/query-service/common"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
opentracing "github.com/opentracing/opentracing-go"
plabels "github.com/prometheus/prometheus/model/labels"
"go.uber.org/zap"
@@ -35,12 +36,13 @@ type PromRuleTask struct {
logger *zap.Logger
notify NotifyFunc
ruleDB RuleDB
maintenanceStore ruletypes.MaintenanceStore
orgID string
}
// newPromRuleTask holds rules that have promql condition
// and evalutes the rule at a given frequency
func NewPromRuleTask(name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc, ruleDB RuleDB) *PromRuleTask {
func NewPromRuleTask(name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID string) *PromRuleTask {
zap.L().Info("Initiating a new rule group", zap.String("name", name), zap.Duration("frequency", frequency))
if time.Now() == time.Now().Add(frequency) {
@@ -58,8 +60,9 @@ func NewPromRuleTask(name, file string, frequency time.Duration, rules []Rule, o
done: make(chan struct{}),
terminated: make(chan struct{}),
notify: notify,
ruleDB: ruleDB,
maintenanceStore: maintenanceStore,
logger: opts.Logger,
orgID: orgID,
}
}
@@ -323,9 +326,7 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
}()
zap.L().Info("promql rule task", zap.String("name", g.name), zap.Time("eval started at", ts))
maintenance, err := g.ruleDB.GetAllPlannedMaintenance(ctx)
maintenance, err := g.maintenanceStore.GetAllPlannedMaintenance(ctx, g.orgID)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
}
@@ -338,7 +339,7 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
shouldSkip := false
for _, m := range maintenance {
zap.L().Info("checking if rule should be skipped", zap.String("rule", rule.ID()), zap.Any("maintenance", m))
if m.shouldSkip(rule.ID(), ts) {
if m.ShouldSkip(rule.ID(), ts) {
shouldSkip = true
break
}
@@ -376,7 +377,7 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
_, err := rule.Eval(ctx, ts)
if err != nil {
rule.SetHealth(HealthBad)
rule.SetHealth(ruletypes.HealthBad)
rule.SetLastError(err)
zap.L().Warn("Evaluating rule failed", zap.String("ruleid", rule.ID()), zap.Error(err))

View File

@@ -5,19 +5,20 @@ import (
"time"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
pql "github.com/prometheus/prometheus/promql"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
)
func TestPromRuleShouldAlert(t *testing.T) {
postableRule := PostableRule{
postableRule := ruletypes.PostableRule{
AlertName: "Test Rule",
AlertType: AlertTypeMetric,
RuleType: RuleTypeProm,
EvalWindow: Duration(5 * time.Minute),
Frequency: Duration(1 * time.Minute),
RuleCondition: &RuleCondition{
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeProm,
EvalWindow: ruletypes.Duration(5 * time.Minute),
Frequency: ruletypes.Duration(1 * time.Minute),
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypePromQL,
PromQueries: map[string]*v3.PromQuery{
@@ -652,8 +653,8 @@ func TestPromRuleShouldAlert(t *testing.T) {
}
for idx, c := range cases {
postableRule.RuleCondition.CompareOp = CompareOp(c.compareOp)
postableRule.RuleCondition.MatchType = MatchType(c.matchType)
postableRule.RuleCondition.CompareOp = ruletypes.CompareOp(c.compareOp)
postableRule.RuleCondition.MatchType = ruletypes.MatchType(c.matchType)
postableRule.RuleCondition.Target = &c.target
rule, err := NewPromRule("69", &postableRule, zap.NewNop(), nil, nil)

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
)
// A Rule encapsulates a vector expression which is evaluated at a specified
@@ -13,16 +14,16 @@ import (
type Rule interface {
ID() string
Name() string
Type() RuleType
Type() ruletypes.RuleType
Labels() labels.BaseLabels
Annotations() labels.BaseLabels
Condition() *RuleCondition
Condition() *ruletypes.RuleCondition
EvalDelay() time.Duration
EvalWindow() time.Duration
HoldDuration() time.Duration
State() model.AlertState
ActiveAlerts() []*Alert
ActiveAlerts() []*ruletypes.Alert
PreferredChannels() []string
@@ -30,8 +31,8 @@ type Rule interface {
String() string
SetLastError(error)
LastError() error
SetHealth(RuleHealth)
Health() RuleHealth
SetHealth(ruletypes.RuleHealth)
Health() ruletypes.RuleHealth
SetEvaluationDuration(time.Duration)
GetEvaluationDuration() time.Duration
SetEvaluationTimestamp(time.Time)

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
opentracing "github.com/opentracing/opentracing-go"
"go.uber.org/zap"
)
@@ -32,13 +33,14 @@ type RuleTask struct {
pause bool
notify NotifyFunc
ruleDB RuleDB
maintenanceStore ruletypes.MaintenanceStore
orgID string
}
const DefaultFrequency = 1 * time.Minute
// NewRuleTask makes a new RuleTask with the given name, options, and rules.
func NewRuleTask(name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc, ruleDB RuleDB) *RuleTask {
func NewRuleTask(name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID string) *RuleTask {
if time.Now() == time.Now().Add(frequency) {
frequency = DefaultFrequency
@@ -46,16 +48,17 @@ func NewRuleTask(name, file string, frequency time.Duration, rules []Rule, opts
zap.L().Info("initiating a new rule task", zap.String("name", name), zap.Duration("frequency", frequency))
return &RuleTask{
name: name,
file: file,
pause: false,
frequency: frequency,
rules: rules,
opts: opts,
done: make(chan struct{}),
terminated: make(chan struct{}),
notify: notify,
ruleDB: ruleDB,
name: name,
file: file,
pause: false,
frequency: frequency,
rules: rules,
opts: opts,
done: make(chan struct{}),
terminated: make(chan struct{}),
notify: notify,
maintenanceStore: maintenanceStore,
orgID: orgID,
}
}
@@ -305,7 +308,7 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
zap.L().Debug("rule task eval started", zap.String("name", g.name), zap.Time("start time", ts))
maintenance, err := g.ruleDB.GetAllPlannedMaintenance(ctx)
maintenance, err := g.maintenanceStore.GetAllPlannedMaintenance(ctx, g.orgID)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
@@ -319,7 +322,7 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
shouldSkip := false
for _, m := range maintenance {
zap.L().Info("checking if rule should be skipped", zap.String("rule", rule.ID()), zap.Any("maintenance", m))
if m.shouldSkip(rule.ID(), ts) {
if m.ShouldSkip(rule.ID(), ts) {
shouldSkip = true
break
}
@@ -357,7 +360,7 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
_, err := rule.Eval(ctx, ts)
if err != nil {
rule.SetHealth(HealthBad)
rule.SetHealth(ruletypes.HealthBad)
rule.SetLastError(err)
zap.L().Warn("Evaluating rule failed", zap.String("ruleid", rule.ID()), zap.Error(err))

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