mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-08 02:39:55 +00:00
Compare commits
7 Commits
v0.80.0
...
quickFilte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6445bc0ad | ||
|
|
ce81ab73b1 | ||
|
|
20f7615a80 | ||
|
|
9777b020c5 | ||
|
|
be72e2ea1d | ||
|
|
38a5a21ff0 | ||
|
|
ca6f90926c |
61
.github/workflows/staging-deployment.yaml
vendored
Normal file
61
.github/workflows/staging-deployment.yaml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
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}"
|
||||
56
.github/workflows/testing-deployment.yaml
vendored
Normal file
56
.github/workflows/testing-deployment.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
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}"
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.80.0
|
||||
image: signoz/signoz:v0.79.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
- --use-logs-new-schema=true
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.80.0
|
||||
image: signoz/signoz:v0.79.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
- --use-logs-new-schema=true
|
||||
|
||||
@@ -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.80.0}
|
||||
image: signoz/signoz:${VERSION:-v0.79.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
199
deploy/docker/docker-compose.testing.yaml
Normal file
199
deploy/docker/docker-compose.testing.yaml
Normal file
@@ -0,0 +1,199 @@
|
||||
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
|
||||
@@ -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.80.0}
|
||||
image: signoz/signoz:${VERSION:-v0.79.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -11,7 +11,6 @@ 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"
|
||||
@@ -75,7 +74,6 @@ 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,
|
||||
})
|
||||
|
||||
@@ -367,7 +367,6 @@ 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)
|
||||
@@ -429,11 +428,11 @@ func (s *Server) initListeners() error {
|
||||
}
|
||||
|
||||
// Start listening on http and private http port concurrently
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
func (s *Server) Start() error {
|
||||
|
||||
// initiate rule manager first
|
||||
if !s.serverOptions.DisableRules {
|
||||
s.ruleManager.Start(ctx)
|
||||
s.ruleManager.Start()
|
||||
} else {
|
||||
zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE")
|
||||
}
|
||||
@@ -517,7 +516,7 @@ func (s *Server) Stop() error {
|
||||
s.opampServer.Stop()
|
||||
|
||||
if s.ruleManager != nil {
|
||||
s.ruleManager.Stop(context.Background())
|
||||
s.ruleManager.Stop()
|
||||
}
|
||||
|
||||
// stop usage manager
|
||||
|
||||
@@ -143,7 +143,7 @@ func main() {
|
||||
zap.L().Fatal("Failed to create server", zap.Error(err))
|
||||
}
|
||||
|
||||
if err := server.Start(context.Background()); err != nil {
|
||||
if err := server.Start(); err != nil {
|
||||
zap.L().Fatal("Could not start server", zap.Error(err))
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ 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"
|
||||
@@ -53,7 +52,7 @@ type AnomalyRule struct {
|
||||
|
||||
func NewAnomalyRule(
|
||||
id string,
|
||||
p *ruletypes.PostableRule,
|
||||
p *baserules.PostableRule,
|
||||
reader interfaces.Reader,
|
||||
cache cache.Cache,
|
||||
opts ...baserules.RuleOption,
|
||||
@@ -61,7 +60,7 @@ func NewAnomalyRule(
|
||||
|
||||
zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts))
|
||||
|
||||
if p.RuleCondition.CompareOp == ruletypes.ValueIsBelow {
|
||||
if p.RuleCondition.CompareOp == baserules.ValueIsBelow {
|
||||
target := -1 * *p.RuleCondition.Target
|
||||
p.RuleCondition.Target = &target
|
||||
}
|
||||
@@ -118,7 +117,7 @@ func NewAnomalyRule(
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) Type() ruletypes.RuleType {
|
||||
func (r *AnomalyRule) Type() baserules.RuleType {
|
||||
return RuleTypeAnomaly
|
||||
}
|
||||
|
||||
@@ -158,7 +157,7 @@ func (r *AnomalyRule) GetSelectedQuery() string {
|
||||
return r.Condition().GetSelectedQueryName()
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (ruletypes.Vector, error) {
|
||||
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (baserules.Vector, error) {
|
||||
|
||||
params, err := r.prepareQueryRange(ts)
|
||||
if err != nil {
|
||||
@@ -185,7 +184,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (rulet
|
||||
}
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
var resultVector baserules.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON)))
|
||||
@@ -214,7 +213,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]*ruletypes.Alert, len(res))
|
||||
var alerts = make(map[uint64]*baserules.Alert, len(res))
|
||||
|
||||
for _, smpl := range res {
|
||||
l := make(map[string]string, len(smpl.Metric))
|
||||
@@ -226,7 +225,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 := ruletypes.AlertTemplateData(l, value, threshold)
|
||||
tmplData := baserules.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}}"
|
||||
@@ -234,7 +233,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 := ruletypes.NewTemplateExpander(
|
||||
tmpl := baserules.NewTemplateExpander(
|
||||
ctx,
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
@@ -279,7 +278,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
alerts[h] = &baserules.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLables: resultLabels,
|
||||
Annotations: annotations,
|
||||
@@ -320,7 +319,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) > ruletypes.ResolvedRetention) {
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > baserules.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != model.StateInactive {
|
||||
@@ -376,10 +375,10 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
|
||||
func (r *AnomalyRule) String() string {
|
||||
|
||||
ar := ruletypes.PostableRule{
|
||||
ar := baserules.PostableRule{
|
||||
AlertName: r.Name(),
|
||||
RuleCondition: r.Condition(),
|
||||
EvalWindow: ruletypes.Duration(r.EvalWindow()),
|
||||
EvalWindow: baserules.Duration(r.EvalWindow()),
|
||||
Labels: r.Labels().Map(),
|
||||
Annotations: r.Annotations().Map(),
|
||||
PreferredChannels: r.PreferredChannels(),
|
||||
|
||||
@@ -8,7 +8,6 @@ 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"
|
||||
)
|
||||
@@ -19,7 +18,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
var task baserules.Task
|
||||
|
||||
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
|
||||
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
if opts.Rule.RuleType == baserules.RuleTypeThreshold {
|
||||
// create a threshold rule
|
||||
tr, err := baserules.NewThresholdRule(
|
||||
ruleId,
|
||||
@@ -38,9 +37,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.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
|
||||
} else if opts.Rule.RuleType == baserules.RuleTypeProm {
|
||||
|
||||
// create promql rule
|
||||
pr, err := baserules.NewPromRule(
|
||||
@@ -59,9 +58,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.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
} else if opts.Rule.RuleType == baserules.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
ar, err := NewAnomalyRule(
|
||||
ruleId,
|
||||
@@ -78,10 +77,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.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, baserules.RuleTypeProm, baserules.RuleTypeThreshold)
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -106,12 +105,12 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
}
|
||||
|
||||
// append name to indicate this is test alert
|
||||
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, ruletypes.TestAlertPostFix)
|
||||
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, baserules.TestAlertPostFix)
|
||||
|
||||
var rule baserules.Rule
|
||||
var err error
|
||||
|
||||
if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
if parsedRule.RuleType == baserules.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)
|
||||
@@ -135,7 +134,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeProm {
|
||||
} else if parsedRule.RuleType == baserules.RuleTypeProm {
|
||||
|
||||
// create promql rule
|
||||
rule, err = baserules.NewPromRule(
|
||||
@@ -153,7 +152,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 == ruletypes.RuleTypeAnomaly {
|
||||
} else if parsedRule.RuleType == baserules.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
rule, err = NewAnomalyRule(
|
||||
alertname,
|
||||
@@ -191,9 +190,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, maintenanceStore ruletypes.MaintenanceStore, orgID string) baserules.Task {
|
||||
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task {
|
||||
if taskType == baserules.TaskTypeCh {
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||
}
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
"autoprefixer": "10.4.19",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"compression-webpack-plugin": "9.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"copy-webpack-plugin": "^8.1.0",
|
||||
"critters-webpack-plugin": "^3.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
@@ -255,7 +255,6 @@
|
||||
"body-parser": "1.20.3",
|
||||
"http-proxy-middleware": "3.0.3",
|
||||
"cross-spawn": "7.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"serialize-javascript": "6.0.2"
|
||||
"cookie": "^0.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
@@ -8,7 +7,7 @@ import put from './put';
|
||||
const save = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
if (props.id && !isEmpty(props.id)) {
|
||||
if (props.id && props.id > 0) {
|
||||
return put({ ...props });
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
},
|
||||
);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const handleAddCreditCard = (): void => {
|
||||
logEvent('Add Credit card modal: Clicked', {
|
||||
source: `intercom icon`,
|
||||
@@ -53,7 +52,7 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ function LaunchChatSupport({
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ 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';
|
||||
@@ -69,14 +68,10 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section className="right-actions">
|
||||
@@ -94,33 +89,31 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
@@ -1,382 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,30 +0,0 @@
|
||||
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,
|
||||
},
|
||||
];
|
||||
@@ -40,5 +40,4 @@ export enum QuickFiltersSource {
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
}
|
||||
|
||||
@@ -27,5 +27,4 @@ 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',
|
||||
}
|
||||
|
||||
@@ -398,23 +398,6 @@ 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['!='],
|
||||
|
||||
@@ -16,51 +16,3 @@ 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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -18,17 +18,16 @@ 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 { convertCompositeQueryToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { convertRawQueriesToTraceSelectedTags } 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 } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -110,11 +109,10 @@ function AllErrors(): JSX.Element {
|
||||
);
|
||||
|
||||
const { queries } = useResourceAttribute();
|
||||
const compositeData = useGetCompositeQueryParam();
|
||||
|
||||
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
||||
{
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, compositeData],
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries],
|
||||
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
||||
getAll({
|
||||
end: maxTime,
|
||||
@@ -125,9 +123,7 @@ function AllErrors(): JSX.Element {
|
||||
orderParam: getUpdatedParams,
|
||||
exceptionType: getUpdatedExceptionType,
|
||||
serviceName: getUpdatedServiceName,
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
@@ -138,7 +134,7 @@ function AllErrors(): JSX.Element {
|
||||
minTime,
|
||||
getUpdatedExceptionType,
|
||||
getUpdatedServiceName,
|
||||
compositeData,
|
||||
queries,
|
||||
],
|
||||
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
||||
getErrorCounts({
|
||||
@@ -146,9 +142,7 @@ function AllErrors(): JSX.Element {
|
||||
start: minTime,
|
||||
exceptionType: getUpdatedExceptionType,
|
||||
serviceName: getUpdatedServiceName,
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
@@ -435,8 +429,12 @@ function AllErrors(): JSX.Element {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!isUndefined(errorCountResponse.data?.payload)) {
|
||||
if (
|
||||
!logEventCalledRef.current &&
|
||||
!isUndefined(errorCountResponse.data?.payload)
|
||||
) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === 'resource_deployment_environment',
|
||||
)?.tagValue;
|
||||
@@ -444,12 +442,9 @@ function AllErrors(): JSX.Element {
|
||||
logEvent('Exception: List page visited', {
|
||||
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||
selectedEnvironments,
|
||||
resourceAttributeUsed: !!compositeData?.builder.queryData?.[0]?.filters
|
||||
.items?.length,
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
resourceAttributeUsed: !!queries?.length,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [errorCountResponse.data?.payload]);
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
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',
|
||||
},
|
||||
];
|
||||
@@ -339,8 +339,6 @@ 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';
|
||||
|
||||
@@ -663,8 +661,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isMessagingQueues() ||
|
||||
isCloudIntegrationPage() ||
|
||||
isInfraMonitoring() ||
|
||||
isApiMonitoringView() ||
|
||||
isExceptionsView()
|
||||
isApiMonitoringView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
} else {
|
||||
logEvent('Billing : Manage Billing', {
|
||||
@@ -342,7 +342,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
});
|
||||
|
||||
manageCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -121,7 +121,7 @@ function CreateRules(): JSX.Element {
|
||||
alertType={alertType}
|
||||
formInstance={formInstance}
|
||||
initialValue={initValues}
|
||||
ruleId=""
|
||||
ruleId={0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||
|
||||
interface EditRulesProps {
|
||||
initialValue: AlertDef;
|
||||
ruleId: string;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
||||
@@ -11,7 +11,6 @@ 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';
|
||||
@@ -157,7 +156,7 @@ function QuerySection({
|
||||
runQuery();
|
||||
logEvent('Alert: Stage and run query', {
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
||||
isNewRule: !ruleId || isEmpty(ruleId),
|
||||
isNewRule: !ruleId || ruleId === 0,
|
||||
ruleId,
|
||||
queryType: queryCategory,
|
||||
});
|
||||
@@ -231,7 +230,7 @@ interface QuerySectionProps {
|
||||
runQuery: VoidFunction;
|
||||
alertDef: AlertDef;
|
||||
panelType: PANEL_TYPES;
|
||||
ruleId: string;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
|
||||
@@ -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 { isEmpty, isEqual } from 'lodash-es';
|
||||
import { 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 || isEmpty(ruleId);
|
||||
const isNewRule = ruleId === 0;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [queryStatus, setQueryStatus] = useState<string>('');
|
||||
@@ -481,7 +481,7 @@ function FormAlertRules({
|
||||
|
||||
try {
|
||||
const apiReq =
|
||||
ruleId && !isEmpty(ruleId)
|
||||
ruleId && ruleId > 0
|
||||
? { data: postableAlert, id: ruleId }
|
||||
: { data: postableAlert };
|
||||
|
||||
@@ -491,7 +491,7 @@ function FormAlertRules({
|
||||
logData = {
|
||||
status: 'success',
|
||||
statusMessage:
|
||||
!ruleId || isEmpty(ruleId) ? t('rule_created') : t('rule_edited'),
|
||||
!ruleId || ruleId === 0 ? 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 || isEmpty(ruleId),
|
||||
isNewRule: !ruleId || ruleId === 0,
|
||||
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 || isEmpty(ruleId),
|
||||
isNewRule: !ruleId || ruleId === 0,
|
||||
ruleId,
|
||||
queryType: currentQuery.queryType,
|
||||
status: statusResponse.status,
|
||||
@@ -700,7 +700,7 @@ function FormAlertRules({
|
||||
alertDef?.broadcastToAll ||
|
||||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
|
||||
|
||||
const isRuleCreated = !ruleId || isEmpty(ruleId);
|
||||
const isRuleCreated = !ruleId || ruleId === 0;
|
||||
|
||||
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 || isEmpty(ruleId),
|
||||
isNewRule: !ruleId || ruleId === 0,
|
||||
ruleId,
|
||||
queryType: currentQuery.queryType,
|
||||
link: url,
|
||||
@@ -881,8 +881,8 @@ function FormAlertRules({
|
||||
type="default"
|
||||
onClick={onCancelHandler}
|
||||
>
|
||||
{(!ruleId || isEmpty(ruleId)) && t('button_cancelchanges')}
|
||||
{ruleId && !isEmpty(ruleId) && t('button_discard')}
|
||||
{ruleId === 0 && t('button_cancelchanges')}
|
||||
{ruleId > 0 && t('button_discard')}
|
||||
</ActionButton>
|
||||
</ButtonContainer>
|
||||
</MainFormContainer>
|
||||
@@ -899,7 +899,7 @@ interface FormAlertRuleProps {
|
||||
alertType?: AlertTypes;
|
||||
formInstance: FormInstance;
|
||||
initialValue: AlertDef;
|
||||
ruleId: string;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default FormAlertRules;
|
||||
|
||||
@@ -153,9 +153,7 @@ export default function AlertRules({
|
||||
<div className="alert-rule-item-name-container home-data-item-name-container">
|
||||
<img
|
||||
src={
|
||||
Math.random() % 2 === 0
|
||||
? '/Icons/eight-ball.svg'
|
||||
: '/Icons/circus-tent.svg'
|
||||
rule.id % 2 === 0 ? '/Icons/eight-ball.svg' : '/Icons/circus-tent.svg'
|
||||
}
|
||||
alt="alert-rules"
|
||||
className="alert-rules-img"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import './Home.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Alert, Button, Popover } from 'antd';
|
||||
import { Button, Popover } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
|
||||
@@ -644,16 +644,6 @@ 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">
|
||||
|
||||
@@ -24,7 +24,7 @@ function DeleteAlert({
|
||||
|
||||
const defaultErrorMessage = 'Something went wrong';
|
||||
|
||||
const onDeleteHandler = async (id: string): Promise<void> => {
|
||||
const onDeleteHandler = async (id: number): Promise<void> => {
|
||||
try {
|
||||
const response = await deleteAlerts({
|
||||
id,
|
||||
|
||||
@@ -25,7 +25,7 @@ function ToggleAlertState({
|
||||
const defaultErrorMessage = 'Something went wrong';
|
||||
|
||||
const onToggleHandler = async (
|
||||
id: string,
|
||||
id: number,
|
||||
disabled: boolean,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,6 @@ 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';
|
||||
@@ -35,6 +34,9 @@ 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;
|
||||
@@ -247,7 +249,7 @@ function TableView({
|
||||
}
|
||||
|
||||
const fieldFilterKey = filterKeyForField(field);
|
||||
if (!RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey)) {
|
||||
if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) {
|
||||
return (
|
||||
<AddToQueryHOC
|
||||
fieldKey={fieldFilterKey}
|
||||
|
||||
@@ -9,7 +9,6 @@ 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';
|
||||
@@ -143,7 +142,7 @@ export function TableViewActions(
|
||||
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
|
||||
{renderFieldContent()}
|
||||
</CopyClipboardHOC>
|
||||
{!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
|
||||
{!isListViewPanel && (
|
||||
<span className="action-btn">
|
||||
<Tooltip title="Filter for value">
|
||||
<Button
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -138,7 +138,7 @@ export const deleteDowntimeHandler = ({
|
||||
export const createEditDowntimeSchedule = async (
|
||||
props: DowntimeScheduleUpdatePayload,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
if (props.id) {
|
||||
if (props.id && props.id > 0) {
|
||||
return updateDowntimeSchedule({ ...props });
|
||||
}
|
||||
return createDowntimeSchedule({ ...props.data });
|
||||
|
||||
@@ -453,7 +453,7 @@ export const Query = memo(function Query({
|
||||
</Col>
|
||||
)}
|
||||
<Col flex="1" className="qb-search-container">
|
||||
{[DataSource.LOGS, DataSource.TRACES].includes(query.dataSource) ? (
|
||||
{query.dataSource === DataSource.LOGS ? (
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
|
||||
@@ -120,7 +120,6 @@ function SpanScopeSelector({ queryName }: SpanScopeSelectorProps): JSX.Element {
|
||||
<Select
|
||||
value={selectedScope}
|
||||
className="span-scope-selector"
|
||||
data-testid="span-scope-selector"
|
||||
onChange={handleScopeChange}
|
||||
options={SELECT_OPTIONS}
|
||||
/>
|
||||
@@ -56,6 +56,7 @@ 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,
|
||||
@@ -83,6 +84,11 @@ function QueryBuilderSearch({
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const isTracesExplorerPage = useMemo(
|
||||
() => pathname === ROUTES.TRACES_EXPLORER,
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const [isEditingTag, setIsEditingTag] = useState(false);
|
||||
|
||||
const {
|
||||
@@ -483,6 +489,7 @@ function QueryBuilderSearch({
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{isTracesExplorerPage && <SpanScopeSelector queryName={query.queryName} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import './QueryBuilderSearchV2.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
@@ -26,7 +25,6 @@ interface ICustomDropdownProps {
|
||||
exampleQueries: TagFilter[];
|
||||
onChange: (value: TagFilter) => void;
|
||||
currentFilterItem?: ITag;
|
||||
isLogsDataSource: boolean;
|
||||
}
|
||||
|
||||
export default function QueryBuilderSearchDropdown(
|
||||
@@ -40,14 +38,11 @@ export default function QueryBuilderSearchDropdown(
|
||||
exampleQueries,
|
||||
options,
|
||||
onChange,
|
||||
isLogsDataSource,
|
||||
} = props;
|
||||
const userOs = getUserOperatingSystem();
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cx('content', { 'non-logs-data-source': !isLogsDataSource })}
|
||||
>
|
||||
<div className="content">
|
||||
{!currentFilterItem?.key ? (
|
||||
<div className="suggested-filters">Suggested Filters</div>
|
||||
) : !currentFilterItem?.op ? (
|
||||
|
||||
@@ -11,11 +11,6 @@
|
||||
.rc-virtual-list-holder {
|
||||
height: 115px;
|
||||
}
|
||||
&.non-logs-data-source {
|
||||
.rc-virtual-list-holder {
|
||||
height: 256px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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,
|
||||
@@ -63,9 +62,7 @@ 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 {
|
||||
@@ -91,7 +88,6 @@ interface QueryBuilderSearchV2Props {
|
||||
className?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
||||
operatorConfigKey?: OperatorConfigKeys;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@@ -125,7 +121,6 @@ function QueryBuilderSearchV2(
|
||||
suffixIcon,
|
||||
whereClauseConfig,
|
||||
hardcodedAttributeKeys,
|
||||
operatorConfigKey,
|
||||
} = props;
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@@ -295,8 +290,7 @@ function QueryBuilderSearchV2(
|
||||
if (
|
||||
isObject(parsedValue) &&
|
||||
parsedValue?.key &&
|
||||
parsedValue?.key?.split(' ').length > 1 &&
|
||||
isLogsDataSource
|
||||
parsedValue?.key?.split(' ').length > 1
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
@@ -411,13 +405,7 @@ function QueryBuilderSearchV2(
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
currentFilterItem?.key,
|
||||
currentFilterItem?.op,
|
||||
currentState,
|
||||
isLogsDataSource,
|
||||
searchValue,
|
||||
],
|
||||
[currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback((value: string) => {
|
||||
@@ -701,29 +689,12 @@ function QueryBuilderSearchV2(
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
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) => ({
|
||||
setDropdownOptions(
|
||||
data?.payload?.attributeKeys?.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})) || []),
|
||||
]);
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (currentState === DropdownState.OPERATOR) {
|
||||
@@ -746,11 +717,15 @@ 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) => ({
|
||||
@@ -764,12 +739,9 @@ 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) {
|
||||
@@ -802,7 +774,6 @@ function QueryBuilderSearchV2(
|
||||
isLogsDataSource,
|
||||
searchValue,
|
||||
suggestionsData?.payload?.attributes,
|
||||
operatorConfigKey,
|
||||
]);
|
||||
|
||||
// keep the query in sync with the selected tags in logs explorer page
|
||||
@@ -936,11 +907,6 @@ function QueryBuilderSearchV2(
|
||||
);
|
||||
};
|
||||
|
||||
const isTracesDataSource = useMemo(
|
||||
() => query.dataSource === DataSource.TRACES,
|
||||
[query.dataSource],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="query-builder-search-v2">
|
||||
<Select
|
||||
@@ -998,7 +964,6 @@ function QueryBuilderSearchV2(
|
||||
exampleQueries={suggestionsData?.payload?.example_queries || []}
|
||||
tags={tags}
|
||||
currentFilterItem={currentFilterItem}
|
||||
isLogsDataSource={isLogsDataSource}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
@@ -1025,7 +990,6 @@ function QueryBuilderSearchV2(
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{isTracesDataSource && <SpanScopeSelector queryName={query.queryName} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1036,7 +1000,6 @@ QueryBuilderSearchV2.defaultProps = {
|
||||
suffixIcon: null,
|
||||
whereClauseConfig: {},
|
||||
hardcodedAttributeKeys: undefined,
|
||||
operatorConfigKey: undefined,
|
||||
};
|
||||
|
||||
export default QueryBuilderSearchV2;
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
/* 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',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,165 +0,0 @@
|
||||
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();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -111,13 +110,3 @@ 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),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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;
|
||||
@@ -230,7 +230,6 @@ 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];
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,7 @@ 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,
|
||||
@@ -28,7 +25,6 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
@@ -151,7 +147,7 @@ function SpanOverview({
|
||||
);
|
||||
}
|
||||
|
||||
export function SpanDuration({
|
||||
function SpanDuration({
|
||||
span,
|
||||
traceMetadata,
|
||||
setSelectedSpan,
|
||||
@@ -170,40 +166,20 @@ export 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
|
||||
@@ -214,7 +190,6 @@ export function SpanDuration({
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
{hasActionButtons && <SpanLineActionButtons span={span} />}
|
||||
<Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}>
|
||||
<Typography.Text
|
||||
className="span-line-text"
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -170,7 +170,11 @@ export const useOptions = (
|
||||
(option, index, self) =>
|
||||
index ===
|
||||
self.findIndex(
|
||||
(o) => o.label === option.label && o.value === option.value, // to remove duplicate & empty options from list
|
||||
(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
|
||||
) && option.value !== '',
|
||||
) || []
|
||||
).map((option) => {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -2,10 +2,7 @@ import {
|
||||
getResourceAttributesTagKeys,
|
||||
getResourceAttributesTagValues,
|
||||
} from 'api/metrics/getResourceAttributes';
|
||||
import {
|
||||
CompositeQueryOperatorsConfig,
|
||||
OperatorConversions,
|
||||
} from 'constants/resourceAttributes';
|
||||
import { OperatorConversions } from 'constants/resourceAttributes';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import {
|
||||
@@ -52,32 +49,6 @@ 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',
|
||||
|
||||
@@ -44,7 +44,7 @@ function AlertActionButtons({
|
||||
const { handleAlertDuplicate } = useAlertRuleDuplicate({
|
||||
alertDetails: (alertDetails as unknown) as AlertDef,
|
||||
});
|
||||
const { handleAlertDelete } = useAlertRuleDelete({ ruleId });
|
||||
const { handleAlertDelete } = useAlertRuleDelete({ ruleId: Number(ruleId) });
|
||||
const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({
|
||||
alertDetails: (alertDetails as unknown) as AlertDef,
|
||||
setUpdatedName,
|
||||
|
||||
@@ -153,7 +153,7 @@ type Props = {
|
||||
export const useGetAlertRuleDetails = (): Props => {
|
||||
const { ruleId } = useAlertHistoryQueryParams();
|
||||
|
||||
const isValidRuleId = ruleId !== null && ruleId !== '';
|
||||
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
@@ -163,7 +163,7 @@ export const useGetAlertRuleDetails = (): Props => {
|
||||
} = useQuery([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId], {
|
||||
queryFn: () =>
|
||||
get({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
}),
|
||||
enabled: isValidRuleId,
|
||||
refetchOnWindowFocus: false,
|
||||
@@ -204,7 +204,7 @@ export const useGetAlertRuleDetailsStats = (): GetAlertRuleDetailsStatsProps =>
|
||||
{
|
||||
queryFn: () =>
|
||||
ruleStats({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
}),
|
||||
@@ -234,7 +234,7 @@ export const useGetAlertRuleDetailsTopContributors = (): GetAlertRuleDetailsTopC
|
||||
{
|
||||
queryFn: () =>
|
||||
topContributors({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
}),
|
||||
@@ -287,7 +287,7 @@ export const useGetAlertRuleDetailsTimelineTable = ({
|
||||
{
|
||||
queryFn: () =>
|
||||
timelineTable({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
limit: TIMELINE_TABLE_PAGE_SIZE,
|
||||
@@ -410,7 +410,7 @@ export const useAlertRuleStatusToggle = ({
|
||||
|
||||
const handleAlertStateToggle = (): void => {
|
||||
const args = {
|
||||
id: ruleId,
|
||||
id: parseInt(ruleId, 10),
|
||||
data: { disabled: alertRuleState !== 'disabled' },
|
||||
};
|
||||
toggleAlertState(args);
|
||||
@@ -512,7 +512,7 @@ export const useAlertRuleUpdate = ({
|
||||
export const useAlertRuleDelete = ({
|
||||
ruleId,
|
||||
}: {
|
||||
ruleId: string;
|
||||
ruleId: number;
|
||||
}): {
|
||||
handleAlertDelete: () => void;
|
||||
} => {
|
||||
@@ -560,7 +560,7 @@ export const useGetAlertRuleDetailsTimelineGraphData = (): GetAlertRuleDetailsTi
|
||||
{
|
||||
queryFn: () =>
|
||||
timelineGraph({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
}),
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +1,18 @@
|
||||
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 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 ResourceAttributesFilter from 'container/ResourceAttributesFilter';
|
||||
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 (
|
||||
<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>
|
||||
<>
|
||||
<ResourceAttributesFilter />
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
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,
|
||||
},
|
||||
];
|
||||
@@ -34,7 +34,7 @@ function EditRules(): JSX.Element {
|
||||
{
|
||||
queryFn: () =>
|
||||
get({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
}),
|
||||
enabled: isValidRuleId,
|
||||
refetchOnMount: false,
|
||||
@@ -90,7 +90,10 @@ function EditRules(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="edit-rules-container">
|
||||
<EditRulesContainer ruleId={ruleId || ''} initialValue={data.payload.data} />
|
||||
<EditRulesContainer
|
||||
ruleId={parseInt(ruleId, 10)}
|
||||
initialValue={data.payload.data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function Support(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
logEvent('Workspace Blocked: User Clicked Update Credit Card', {});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updateCreditCard]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import './LineClampedText.styles.scss';
|
||||
|
||||
import { Tooltip, TooltipProps } from 'antd';
|
||||
import { isBoolean } from 'lodash-es';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
function LineClampedText({
|
||||
@@ -9,7 +8,7 @@ function LineClampedText({
|
||||
lines,
|
||||
tooltipProps,
|
||||
}: {
|
||||
text: string | boolean;
|
||||
text: string;
|
||||
lines?: number;
|
||||
tooltipProps?: TooltipProps;
|
||||
}): JSX.Element {
|
||||
@@ -41,7 +40,7 @@ function LineClampedText({
|
||||
WebkitLineClamp: lines,
|
||||
}}
|
||||
>
|
||||
{isBoolean(text) ? String(text) : text}
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ export const defaultAlgorithm = 'standard';
|
||||
export const defaultSeasonality = 'hourly';
|
||||
|
||||
export interface AlertDef {
|
||||
id?: string;
|
||||
id?: number;
|
||||
alertType?: string;
|
||||
alert: string;
|
||||
ruleType?: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface Props {
|
||||
}
|
||||
|
||||
export interface GettableAlert extends AlertDef {
|
||||
id: string;
|
||||
id: number;
|
||||
alert: string;
|
||||
state: string;
|
||||
disabled: boolean;
|
||||
|
||||
@@ -7,6 +7,6 @@ export interface PatchProps {
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
id?: string;
|
||||
id?: number;
|
||||
data: PatchProps;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ export type PayloadProps = {
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
id?: string;
|
||||
id?: number;
|
||||
data: AlertDef;
|
||||
}
|
||||
|
||||
@@ -6852,17 +6852,18 @@ copy-to-clipboard@^3.3.1, copy-to-clipboard@^3.3.3:
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
fast-glob "^3.2.11"
|
||||
glob-parent "^6.0.1"
|
||||
globby "^13.1.1"
|
||||
fast-glob "^3.2.5"
|
||||
glob-parent "^5.1.1"
|
||||
globby "^11.0.3"
|
||||
normalize-path "^3.0.0"
|
||||
schema-utils "^4.0.0"
|
||||
serialize-javascript "^6.0.0"
|
||||
p-limit "^3.1.0"
|
||||
schema-utils "^3.0.0"
|
||||
serialize-javascript "^5.0.1"
|
||||
|
||||
core-js-compat@^3.25.1:
|
||||
version "3.30.1"
|
||||
@@ -8739,7 +8740,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.2.11, fast-glob@^3.3.0:
|
||||
fast-glob@^3.0.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
|
||||
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
|
||||
@@ -8750,7 +8751,7 @@ fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.3.0:
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.8"
|
||||
|
||||
fast-glob@^3.2.9:
|
||||
fast-glob@^3.2.5, 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==
|
||||
@@ -9306,20 +9307,13 @@ 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.2, glob-parent@~5.1.2:
|
||||
glob-parent@^5.1.1, 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"
|
||||
@@ -9407,17 +9401,6 @@ 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"
|
||||
@@ -10078,11 +10061,6 @@ 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"
|
||||
@@ -13423,7 +13401,7 @@ p-limit@^2.2.0:
|
||||
dependencies:
|
||||
p-try "^2.0.0"
|
||||
|
||||
p-limit@^3.0.2:
|
||||
p-limit@^3.0.2, p-limit@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
|
||||
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
|
||||
@@ -15867,10 +15845,17 @@ send@0.19.0:
|
||||
range-parser "~1.2.1"
|
||||
statuses "2.0.1"
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
@@ -16027,11 +16012,6 @@ 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"
|
||||
|
||||
@@ -3,6 +3,7 @@ package sqlalertmanagerstore
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
@@ -190,9 +191,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:rule"`
|
||||
ID valuer.UUID `bun:"id,pk"`
|
||||
Data string `bun:"data"`
|
||||
bun.BaseModel `bun:"table:rules"`
|
||||
ID int `bun:"id,pk"`
|
||||
Data string `bun:"data"`
|
||||
}
|
||||
|
||||
matchers := []matcher{}
|
||||
@@ -212,7 +213,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[matcher.ID.StringValue()] = append(matchersMap[matcher.ID.StringValue()], receiver.String())
|
||||
matchersMap[strconv.Itoa(matcher.ID)] = append(matchersMap[strconv.Itoa(matcher.ID)], receiver.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,17 +160,6 @@ 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
|
||||
@@ -202,38 +191,6 @@ 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]
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -19,7 +19,6 @@ 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"
|
||||
@@ -60,7 +59,6 @@ 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"
|
||||
|
||||
@@ -143,8 +141,6 @@ type APIHandler struct {
|
||||
|
||||
AlertmanagerAPI *alertmanager.API
|
||||
|
||||
FieldsAPI *fields.API
|
||||
|
||||
Signoz *signoz.SigNoz
|
||||
|
||||
Preference preference.API
|
||||
@@ -192,8 +188,6 @@ type APIHandlerOpts struct {
|
||||
|
||||
AlertmanagerAPI *alertmanager.API
|
||||
|
||||
FieldsAPI *fields.API
|
||||
|
||||
Signoz *signoz.SigNoz
|
||||
|
||||
Preference preference.API
|
||||
@@ -266,7 +260,6 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
|
||||
AlertmanagerAPI: opts.AlertmanagerAPI,
|
||||
Signoz: opts.Signoz,
|
||||
Preference: opts.Preference,
|
||||
FieldsAPI: opts.FieldsAPI,
|
||||
}
|
||||
|
||||
logsQueryBuilder := logsv3.PrepareLogsQuery
|
||||
@@ -423,13 +416,6 @@ 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)
|
||||
@@ -748,15 +734,9 @@ func (aH *APIHandler) PopulateTemporality(ctx context.Context, qp *v3.QueryRange
|
||||
}
|
||||
|
||||
func (aH *APIHandler) listDowntimeSchedules(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
schedules, err := aH.ruleManager.RuleDB().GetAllPlannedMaintenance(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -764,7 +744,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([]*ruletypes.GettablePlannedMaintenance, 0)
|
||||
activeSchedules := make([]rules.PlannedMaintenance, 0)
|
||||
active, _ := strconv.ParseBool(r.URL.Query().Get("active"))
|
||||
for _, schedule := range schedules {
|
||||
now := time.Now().In(time.FixedZone(schedule.Schedule.Timezone, 0))
|
||||
@@ -776,7 +756,7 @@ func (aH *APIHandler) listDowntimeSchedules(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("recurring") != "" {
|
||||
recurringSchedules := make([]*ruletypes.GettablePlannedMaintenance, 0)
|
||||
recurringSchedules := make([]rules.PlannedMaintenance, 0)
|
||||
recurring, _ := strconv.ParseBool(r.URL.Query().Get("recurring"))
|
||||
for _, schedule := range schedules {
|
||||
if schedule.IsRecurring() == recurring {
|
||||
@@ -790,83 +770,62 @@ func (aH *APIHandler) listDowntimeSchedules(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
id := mux.Vars(r)["id"]
|
||||
schedule, err := aH.ruleManager.RuleDB().GetPlannedMaintenanceByID(r.Context(), id)
|
||||
if 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)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, schedule)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) createDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
var schedule ruletypes.GettablePlannedMaintenance
|
||||
var schedule rules.PlannedMaintenance
|
||||
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 {
|
||||
render.Error(w, err)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = aH.ruleManager.MaintenanceStore().CreatePlannedMaintenance(r.Context(), schedule)
|
||||
_, err = aH.ruleManager.RuleDB().CreatePlannedMaintenance(r.Context(), schedule)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, nil)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) editDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
id := mux.Vars(r)["id"]
|
||||
var schedule rules.PlannedMaintenance
|
||||
err := json.NewDecoder(r.Body).Decode(&schedule)
|
||||
if 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)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
if err := schedule.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = aH.ruleManager.MaintenanceStore().EditPlannedMaintenance(r.Context(), schedule, id)
|
||||
_, err = aH.ruleManager.RuleDB().EditPlannedMaintenance(r.Context(), schedule, id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, nil)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) deleteDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
id := mux.Vars(r)["id"]
|
||||
_, err := aH.ruleManager.RuleDB().DeletePlannedMaintenance(r.Context(), id)
|
||||
if err != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = aH.ruleManager.MaintenanceStore().DeletePlannedMaintenance(r.Context(), id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, nil)
|
||||
}
|
||||
|
||||
@@ -970,12 +929,12 @@ func (aH *APIHandler) getOverallStateTransitions(w http.ResponseWriter, r *http.
|
||||
aH.Respond(w, stateItems)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) metaForLinks(ctx context.Context, rule *ruletypes.GettableRule) ([]v3.FilterItem, []v3.AttributeKey, map[string]v3.AttributeKey) {
|
||||
func (aH *APIHandler) metaForLinks(ctx context.Context, rule *rules.GettableRule) ([]v3.FilterItem, []v3.AttributeKey, map[string]v3.AttributeKey) {
|
||||
filterItems := []v3.FilterItem{}
|
||||
groupBy := []v3.AttributeKey{}
|
||||
keys := make(map[string]v3.AttributeKey)
|
||||
|
||||
if rule.AlertType == ruletypes.AlertTypeLogs {
|
||||
if rule.AlertType == rules.AlertTypeLogs {
|
||||
logFields, err := aH.reader.GetLogFields(ctx)
|
||||
if err == nil {
|
||||
params := &v3.QueryRangeParamsV3{
|
||||
@@ -985,7 +944,7 @@ func (aH *APIHandler) metaForLinks(ctx context.Context, rule *ruletypes.Gettable
|
||||
} 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 == ruletypes.AlertTypeTraces {
|
||||
} else if rule.AlertType == rules.AlertTypeTraces {
|
||||
traceFields, err := aH.reader.GetSpanAttributeKeys(ctx)
|
||||
if err == nil {
|
||||
keys = traceFields
|
||||
@@ -994,7 +953,7 @@ func (aH *APIHandler) metaForLinks(ctx context.Context, rule *ruletypes.Gettable
|
||||
}
|
||||
}
|
||||
|
||||
if rule.AlertType == ruletypes.AlertTypeLogs || rule.AlertType == ruletypes.AlertTypeTraces {
|
||||
if rule.AlertType == rules.AlertTypeLogs || rule.AlertType == rules.AlertTypeTraces {
|
||||
if rule.RuleCondition.CompositeQuery != nil {
|
||||
if rule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
|
||||
selectedQuery := rule.RuleCondition.GetSelectedQueryName()
|
||||
@@ -1047,9 +1006,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 == ruletypes.AlertTypeLogs {
|
||||
if rule.AlertType == rules.AlertTypeLogs {
|
||||
res.Items[idx].RelatedLogsLink = contextlinks.PrepareLinksToLogs(start, end, newFilters)
|
||||
} else if rule.AlertType == ruletypes.AlertTypeTraces {
|
||||
} else if rule.AlertType == rules.AlertTypeTraces {
|
||||
res.Items[idx].RelatedTracesLink = contextlinks.PrepareLinksToTraces(start, end, newFilters)
|
||||
}
|
||||
}
|
||||
@@ -1085,9 +1044,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 == ruletypes.AlertTypeLogs {
|
||||
if rule.AlertType == rules.AlertTypeLogs {
|
||||
res[idx].RelatedLogsLink = contextlinks.PrepareLinksToLogs(start, end, newFilters)
|
||||
} else if rule.AlertType == ruletypes.AlertTypeTraces {
|
||||
} else if rule.AlertType == rules.AlertTypeTraces {
|
||||
res[idx].RelatedTracesLink = contextlinks.PrepareLinksToTraces(start, end, newFilters)
|
||||
}
|
||||
}
|
||||
@@ -5600,12 +5559,7 @@ func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err = postprocess.PostProcessResult(result, queryRangeParams)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
return
|
||||
}
|
||||
result = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
|
||||
|
||||
if !thirdPartyQueryRequest.ShowIp {
|
||||
result = thirdPartyApi.FilterResponse(result)
|
||||
|
||||
@@ -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 []ruletypes.PostableRule `json:"alerts"`
|
||||
Alerts []rules.PostableRule `json:"alerts"`
|
||||
}
|
||||
|
||||
type LogsAssets struct {
|
||||
|
||||
@@ -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: []ruletypes.PostableRule{},
|
||||
Alerts: []rules.PostableRule{},
|
||||
},
|
||||
ConnectionTests: &IntegrationConnectionTests{
|
||||
Logs: &LogsConnectionTest{
|
||||
@@ -197,7 +197,7 @@ func (t *TestAvailableIntegrationsRepo) list(
|
||||
},
|
||||
},
|
||||
Dashboards: []types.DashboardData{},
|
||||
Alerts: []ruletypes.PostableRule{},
|
||||
Alerts: []rules.PostableRule{},
|
||||
},
|
||||
ConnectionTests: &IntegrationConnectionTests{
|
||||
Logs: &LogsConnectionTest{
|
||||
|
||||
@@ -58,7 +58,6 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
|
||||
|
||||
builderQueries["endpoints"] = &v3.BuilderQuery{
|
||||
QueryName: "endpoints",
|
||||
Legend: "endpoints",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorCountDistinct,
|
||||
@@ -92,14 +91,11 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
}
|
||||
|
||||
builderQueries["lastseen"] = &v3.BuilderQuery{
|
||||
QueryName: "lastseen",
|
||||
Legend: "lastseen",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorMax,
|
||||
@@ -131,14 +127,11 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
}
|
||||
|
||||
builderQueries["rps"] = &v3.BuilderQuery{
|
||||
QueryName: "rps",
|
||||
Legend: "rps",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorRate,
|
||||
@@ -170,22 +163,18 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
}
|
||||
|
||||
builderQueries["error"] = &v3.BuilderQuery{
|
||||
QueryName: "error",
|
||||
builderQueries["error_rate"] = &v3.BuilderQuery{
|
||||
QueryName: "error_rate",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorCountDistinct,
|
||||
AggregateOperator: v3.AggregateOperatorRate,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "span_id",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
Key: "",
|
||||
},
|
||||
TimeAggregation: v3.TimeAggregationCountDistinct,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
@@ -211,7 +200,7 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "error",
|
||||
Expression: "error_rate",
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{
|
||||
{
|
||||
Key: "net.peer.name",
|
||||
@@ -219,56 +208,11 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
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,
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
}
|
||||
|
||||
builderQueries["p99"] = &v3.BuilderQuery{
|
||||
QueryName: "p99",
|
||||
Legend: "p99",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorP99,
|
||||
@@ -302,18 +246,7 @@ func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, er
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
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,
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
}
|
||||
|
||||
compositeQuery := &v3.CompositeQuery{
|
||||
|
||||
@@ -26,7 +26,6 @@ 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",
|
||||
|
||||
@@ -282,7 +282,7 @@ func orderByAttributeKeyTags(panelType v3.PanelType, items []v3.OrderBy, tags []
|
||||
return str
|
||||
}
|
||||
|
||||
func generateAggregateClause(panelType v3.PanelType, start, end int64, aggOp v3.AggregateOperator,
|
||||
func generateAggregateClause(aggOp v3.AggregateOperator,
|
||||
aggKey string,
|
||||
step int64,
|
||||
timeFilter string,
|
||||
@@ -296,20 +296,18 @@ func generateAggregateClause(panelType v3.PanelType, start, end int64, aggOp v3.
|
||||
"%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.AggregateOperatorRate:
|
||||
v3.AggregateOperatorRateMin:
|
||||
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)
|
||||
@@ -420,7 +418,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(panelType, logsStart, logsEnd, mq.AggregateOperator, aggregationKey, step, timeFilter, filterSubQuery, groupBy, having, orderBy)
|
||||
aggClause, err := generateAggregateClause(mq.AggregateOperator, aggregationKey, step, timeFilter, filterSubQuery, groupBy, having, orderBy)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -571,9 +571,6 @@ 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
|
||||
@@ -593,7 +590,6 @@ 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)",
|
||||
@@ -610,7 +606,6 @@ 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)",
|
||||
@@ -623,29 +618,10 @@ 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.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)
|
||||
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)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("generateAggreagteClause() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
@@ -13,7 +13,6 @@ 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"
|
||||
@@ -202,7 +201,6 @@ 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,
|
||||
})
|
||||
@@ -323,7 +321,6 @@ 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)
|
||||
@@ -385,11 +382,11 @@ func (s *Server) initListeners() error {
|
||||
}
|
||||
|
||||
// Start listening on http and private http port concurrently
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
func (s *Server) Start() error {
|
||||
|
||||
// initiate rule manager first
|
||||
if !s.serverOptions.DisableRules {
|
||||
s.ruleManager.Start(ctx)
|
||||
s.ruleManager.Start()
|
||||
} else {
|
||||
zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE")
|
||||
}
|
||||
@@ -457,7 +454,7 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
func (s *Server) Stop() error {
|
||||
if s.httpServer != nil {
|
||||
if err := s.httpServer.Shutdown(context.Background()); err != nil {
|
||||
return err
|
||||
@@ -473,7 +470,7 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
s.opampServer.Stop()
|
||||
|
||||
if s.ruleManager != nil {
|
||||
s.ruleManager.Stop(ctx)
|
||||
s.ruleManager.Stop()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -367,15 +367,8 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
rate := float64(step)
|
||||
op := fmt.Sprintf("%s(%s)/%f", tracesV3.AggregateOperatorToSQLFunc[mq.AggregateOperator], aggregationKey, rate)
|
||||
query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy)
|
||||
return query, nil
|
||||
|
||||
@@ -693,38 +693,6 @@ 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) {
|
||||
|
||||
@@ -135,7 +135,7 @@ func main() {
|
||||
logger.Fatal("Failed to create server", zap.Error(err))
|
||||
}
|
||||
|
||||
if err := server.Start(context.Background()); err != nil {
|
||||
if err := server.Start(); 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(context.Background())
|
||||
err = server.Stop()
|
||||
if err != nil {
|
||||
zap.L().Fatal("Failed to stop server", zap.Error(err))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ruletypes
|
||||
package rules
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"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/utils/labels"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// this file contains common structs and methods used by
|
||||
@@ -19,7 +20,8 @@ import (
|
||||
const (
|
||||
// how long before re-sending the alert
|
||||
ResolvedRetention = 15 * time.Minute
|
||||
TestAlertPostFix = "_TEST_ALERT"
|
||||
|
||||
TestAlertPostFix = "_TEST_ALERT"
|
||||
)
|
||||
|
||||
type RuleType string
|
||||
@@ -61,7 +63,7 @@ type Alert struct {
|
||||
Missing bool
|
||||
}
|
||||
|
||||
func (a *Alert) NeedsSending(ts time.Time, resendDelay time.Duration) bool {
|
||||
func (a *Alert) needsSending(ts time.Time, resendDelay time.Duration) bool {
|
||||
if a.State == model.StatePending {
|
||||
return false
|
||||
}
|
||||
@@ -199,11 +201,39 @@ func (rc *RuleCondition) String() string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type Duration time.Duration
|
||||
|
||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Duration(d).String())
|
||||
}
|
||||
|
||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||
var v interface{}
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
switch value := v.(type) {
|
||||
case float64:
|
||||
*d = Duration(time.Duration(value))
|
||||
return nil
|
||||
case string:
|
||||
tmp, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = Duration(tmp)
|
||||
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid duration")
|
||||
}
|
||||
}
|
||||
|
||||
// prepareRuleGeneratorURL creates an appropriate url
|
||||
// for the rule. the URL is sent in slack messages as well as
|
||||
// to other systems and allows backtracking to the rule definition
|
||||
// from the third party systems.
|
||||
func PrepareRuleGeneratorURL(ruleId string, source string) string {
|
||||
func prepareRuleGeneratorURL(ruleId string, source string) string {
|
||||
if source == "" {
|
||||
return source
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ruletypes
|
||||
package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -70,16 +70,16 @@ type PostableRule struct {
|
||||
}
|
||||
|
||||
func ParsePostableRule(content []byte) (*PostableRule, error) {
|
||||
return ParsePostableRuleWithKind(content, "json")
|
||||
return parsePostableRule(content, "json")
|
||||
}
|
||||
|
||||
func ParsePostableRuleWithKind(content []byte, kind RuleDataKind) (*PostableRule, error) {
|
||||
return ParseIntoRule(PostableRule{}, content, kind)
|
||||
func parsePostableRule(content []byte, kind RuleDataKind) (*PostableRule, error) {
|
||||
return parseIntoRule(PostableRule{}, content, kind)
|
||||
}
|
||||
|
||||
// parseIntoRule loads the content (data) into PostableRule and also
|
||||
// validates the end result
|
||||
func ParseIntoRule(initRule PostableRule, content []byte, kind RuleDataKind) (*PostableRule, error) {
|
||||
func parseIntoRule(initRule PostableRule, content []byte, kind RuleDataKind) (*PostableRule, error) {
|
||||
rule := &initRule
|
||||
|
||||
var err error
|
||||
@@ -1,4 +1,4 @@
|
||||
package ruletypes
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -14,7 +14,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -26,9 +25,9 @@ type BaseRule struct {
|
||||
handledRestart bool
|
||||
|
||||
// Type of the rule
|
||||
typ ruletypes.AlertType
|
||||
typ AlertType
|
||||
|
||||
ruleCondition *ruletypes.RuleCondition
|
||||
ruleCondition *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
|
||||
@@ -53,9 +52,9 @@ type BaseRule struct {
|
||||
// the timestamp of the last evaluation
|
||||
evaluationTimestamp time.Time
|
||||
|
||||
health ruletypes.RuleHealth
|
||||
health RuleHealth
|
||||
lastError error
|
||||
Active map[uint64]*ruletypes.Alert
|
||||
Active map[uint64]*Alert
|
||||
|
||||
// lastTimestampWithDatapoints is the timestamp of the last datapoint we observed
|
||||
// for this rule
|
||||
@@ -116,7 +115,7 @@ func WithSQLStore(sqlstore sqlstore.SQLStore) RuleOption {
|
||||
}
|
||||
}
|
||||
|
||||
func NewBaseRule(id string, p *ruletypes.PostableRule, reader interfaces.Reader, opts ...RuleOption) (*BaseRule, error) {
|
||||
func NewBaseRule(id string, p *PostableRule, reader interfaces.Reader, opts ...RuleOption) (*BaseRule, error) {
|
||||
if p.RuleCondition == nil || !p.RuleCondition.IsValid() {
|
||||
return nil, fmt.Errorf("invalid rule condition")
|
||||
}
|
||||
@@ -131,8 +130,8 @@ func NewBaseRule(id string, p *ruletypes.PostableRule, reader interfaces.Reader,
|
||||
labels: qslabels.FromMap(p.Labels),
|
||||
annotations: qslabels.FromMap(p.Annotations),
|
||||
preferredChannels: p.PreferredChannels,
|
||||
health: ruletypes.HealthUnknown,
|
||||
Active: map[uint64]*ruletypes.Alert{},
|
||||
health: HealthUnknown,
|
||||
Active: map[uint64]*Alert{},
|
||||
reader: reader,
|
||||
TemporalityMap: make(map[string]map[v3.Temporality]bool),
|
||||
}
|
||||
@@ -164,25 +163,25 @@ func (r *BaseRule) targetVal() float64 {
|
||||
return value.F
|
||||
}
|
||||
|
||||
func (r *BaseRule) matchType() ruletypes.MatchType {
|
||||
func (r *BaseRule) matchType() MatchType {
|
||||
if r.ruleCondition == nil {
|
||||
return ruletypes.AtleastOnce
|
||||
return AtleastOnce
|
||||
}
|
||||
return r.ruleCondition.MatchType
|
||||
}
|
||||
|
||||
func (r *BaseRule) compareOp() ruletypes.CompareOp {
|
||||
func (r *BaseRule) compareOp() CompareOp {
|
||||
if r.ruleCondition == nil {
|
||||
return ruletypes.ValueIsEq
|
||||
return ValueIsEq
|
||||
}
|
||||
return r.ruleCondition.CompareOp
|
||||
}
|
||||
|
||||
func (r *BaseRule) currentAlerts() []*ruletypes.Alert {
|
||||
func (r *BaseRule) currentAlerts() []*Alert {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
alerts := make([]*ruletypes.Alert, 0, len(r.Active))
|
||||
alerts := make([]*Alert, 0, len(r.Active))
|
||||
for _, a := range r.Active {
|
||||
anew := *a
|
||||
alerts = append(alerts, &anew)
|
||||
@@ -217,15 +216,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() *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) 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) GeneratorURL() string {
|
||||
return ruletypes.PrepareRuleGeneratorURL(r.ID(), r.source)
|
||||
return prepareRuleGeneratorURL(r.ID(), r.source)
|
||||
}
|
||||
|
||||
func (r *BaseRule) Unit() string {
|
||||
@@ -262,13 +261,13 @@ func (r *BaseRule) LastError() error {
|
||||
return r.lastError
|
||||
}
|
||||
|
||||
func (r *BaseRule) SetHealth(health ruletypes.RuleHealth) {
|
||||
func (r *BaseRule) SetHealth(health RuleHealth) {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
r.health = health
|
||||
}
|
||||
|
||||
func (r *BaseRule) Health() ruletypes.RuleHealth {
|
||||
func (r *BaseRule) Health() RuleHealth {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
return r.health
|
||||
@@ -308,8 +307,8 @@ func (r *BaseRule) State() model.AlertState {
|
||||
return maxState
|
||||
}
|
||||
|
||||
func (r *BaseRule) ActiveAlerts() []*ruletypes.Alert {
|
||||
var res []*ruletypes.Alert
|
||||
func (r *BaseRule) ActiveAlerts() []*Alert {
|
||||
var res []*Alert
|
||||
for _, a := range r.currentAlerts() {
|
||||
if a.ResolvedAt.IsZero() {
|
||||
res = append(res, a)
|
||||
@@ -333,9 +332,9 @@ func (r *BaseRule) SendAlerts(ctx context.Context, ts time.Time, resendDelay tim
|
||||
return
|
||||
}
|
||||
|
||||
alerts := []*ruletypes.Alert{}
|
||||
r.ForEachActiveAlert(func(alert *ruletypes.Alert) {
|
||||
if alert.NeedsSending(ts, resendDelay) {
|
||||
alerts := []*Alert{}
|
||||
r.ForEachActiveAlert(func(alert *Alert) {
|
||||
if alert.needsSending(ts, resendDelay) {
|
||||
alert.LastSentAt = ts
|
||||
delta := resendDelay
|
||||
if interval > resendDelay {
|
||||
@@ -349,7 +348,7 @@ func (r *BaseRule) SendAlerts(ctx context.Context, ts time.Time, resendDelay tim
|
||||
notifyFunc(ctx, orgID, "", alerts...)
|
||||
}
|
||||
|
||||
func (r *BaseRule) ForEachActiveAlert(f func(*ruletypes.Alert)) {
|
||||
func (r *BaseRule) ForEachActiveAlert(f func(*Alert)) {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
@@ -358,8 +357,8 @@ func (r *BaseRule) ForEachActiveAlert(f func(*ruletypes.Alert)) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *BaseRule) ShouldAlert(series v3.Series) (ruletypes.Sample, bool) {
|
||||
var alertSmpl ruletypes.Sample
|
||||
func (r *BaseRule) ShouldAlert(series v3.Series) (Sample, bool) {
|
||||
var alertSmpl Sample
|
||||
var shouldAlert bool
|
||||
var lbls qslabels.Labels
|
||||
|
||||
@@ -382,54 +381,54 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (ruletypes.Sample, bool) {
|
||||
}
|
||||
|
||||
switch r.matchType() {
|
||||
case ruletypes.AtleastOnce:
|
||||
case AtleastOnce:
|
||||
// If any sample matches the condition, the rule is firing.
|
||||
if r.compareOp() == ruletypes.ValueIsAbove {
|
||||
if r.compareOp() == ValueIsAbove {
|
||||
for _, smpl := range series.Points {
|
||||
if smpl.Value > r.targetVal() {
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
|
||||
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
|
||||
shouldAlert = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsBelow {
|
||||
} else if r.compareOp() == ValueIsBelow {
|
||||
for _, smpl := range series.Points {
|
||||
if smpl.Value < r.targetVal() {
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
|
||||
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
|
||||
shouldAlert = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsEq {
|
||||
} else if r.compareOp() == ValueIsEq {
|
||||
for _, smpl := range series.Points {
|
||||
if smpl.Value == r.targetVal() {
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
|
||||
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
|
||||
shouldAlert = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsNotEq {
|
||||
} else if r.compareOp() == ValueIsNotEq {
|
||||
for _, smpl := range series.Points {
|
||||
if smpl.Value != r.targetVal() {
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
|
||||
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
|
||||
shouldAlert = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
|
||||
} else if r.compareOp() == ValueOutsideBounds {
|
||||
for _, smpl := range series.Points {
|
||||
if math.Abs(smpl.Value) >= r.targetVal() {
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
|
||||
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
|
||||
shouldAlert = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case ruletypes.AllTheTimes:
|
||||
case AllTheTimes:
|
||||
// If all samples match the condition, the rule is firing.
|
||||
shouldAlert = true
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: r.targetVal()}, Metric: lbls}
|
||||
if r.compareOp() == ruletypes.ValueIsAbove {
|
||||
alertSmpl = Sample{Point: Point{V: r.targetVal()}, Metric: lbls}
|
||||
if r.compareOp() == ValueIsAbove {
|
||||
for _, smpl := range series.Points {
|
||||
if smpl.Value <= r.targetVal() {
|
||||
shouldAlert = false
|
||||
@@ -444,9 +443,9 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (ruletypes.Sample, bool) {
|
||||
minValue = smpl.Value
|
||||
}
|
||||
}
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: minValue}, Metric: lbls}
|
||||
alertSmpl = Sample{Point: Point{V: minValue}, Metric: lbls}
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsBelow {
|
||||
} else if r.compareOp() == ValueIsBelow {
|
||||
for _, smpl := range series.Points {
|
||||
if smpl.Value >= r.targetVal() {
|
||||
shouldAlert = false
|
||||
@@ -460,16 +459,16 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (ruletypes.Sample, bool) {
|
||||
maxValue = smpl.Value
|
||||
}
|
||||
}
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: maxValue}, Metric: lbls}
|
||||
alertSmpl = Sample{Point: Point{V: maxValue}, Metric: lbls}
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsEq {
|
||||
} else if r.compareOp() == ValueIsEq {
|
||||
for _, smpl := range series.Points {
|
||||
if smpl.Value != r.targetVal() {
|
||||
shouldAlert = false
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsNotEq {
|
||||
} else if r.compareOp() == ValueIsNotEq {
|
||||
for _, smpl := range series.Points {
|
||||
if smpl.Value == r.targetVal() {
|
||||
shouldAlert = false
|
||||
@@ -480,21 +479,21 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (ruletypes.Sample, bool) {
|
||||
if shouldAlert {
|
||||
for _, smpl := range series.Points {
|
||||
if !math.IsInf(smpl.Value, 0) && !math.IsNaN(smpl.Value) {
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
|
||||
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
|
||||
} else if r.compareOp() == ValueOutsideBounds {
|
||||
for _, smpl := range series.Points {
|
||||
if math.Abs(smpl.Value) < r.targetVal() {
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: smpl.Value}, Metric: lbls}
|
||||
alertSmpl = Sample{Point: Point{V: smpl.Value}, Metric: lbls}
|
||||
shouldAlert = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case ruletypes.OnAverage:
|
||||
case OnAverage:
|
||||
// If the average of all samples matches the condition, the rule is firing.
|
||||
var sum, count float64
|
||||
for _, smpl := range series.Points {
|
||||
@@ -505,29 +504,29 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (ruletypes.Sample, bool) {
|
||||
count++
|
||||
}
|
||||
avg := sum / count
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: avg}, Metric: lbls}
|
||||
if r.compareOp() == ruletypes.ValueIsAbove {
|
||||
alertSmpl = Sample{Point: Point{V: avg}, Metric: lbls}
|
||||
if r.compareOp() == ValueIsAbove {
|
||||
if avg > r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsBelow {
|
||||
} else if r.compareOp() == ValueIsBelow {
|
||||
if avg < r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsEq {
|
||||
} else if r.compareOp() == ValueIsEq {
|
||||
if avg == r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsNotEq {
|
||||
} else if r.compareOp() == ValueIsNotEq {
|
||||
if avg != r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
|
||||
} else if r.compareOp() == ValueOutsideBounds {
|
||||
if math.Abs(avg) >= r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
}
|
||||
case ruletypes.InTotal:
|
||||
case InTotal:
|
||||
// If the sum of all samples matches the condition, the rule is firing.
|
||||
var sum float64
|
||||
|
||||
@@ -537,45 +536,45 @@ func (r *BaseRule) ShouldAlert(series v3.Series) (ruletypes.Sample, bool) {
|
||||
}
|
||||
sum += smpl.Value
|
||||
}
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: sum}, Metric: lbls}
|
||||
if r.compareOp() == ruletypes.ValueIsAbove {
|
||||
alertSmpl = Sample{Point: Point{V: sum}, Metric: lbls}
|
||||
if r.compareOp() == ValueIsAbove {
|
||||
if sum > r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsBelow {
|
||||
} else if r.compareOp() == ValueIsBelow {
|
||||
if sum < r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsEq {
|
||||
} else if r.compareOp() == ValueIsEq {
|
||||
if sum == r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsNotEq {
|
||||
} else if r.compareOp() == ValueIsNotEq {
|
||||
if sum != r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueOutsideBounds {
|
||||
} else if r.compareOp() == ValueOutsideBounds {
|
||||
if math.Abs(sum) >= r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
}
|
||||
case ruletypes.Last:
|
||||
case Last:
|
||||
// If the last sample matches the condition, the rule is firing.
|
||||
shouldAlert = false
|
||||
alertSmpl = ruletypes.Sample{Point: ruletypes.Point{V: series.Points[len(series.Points)-1].Value}, Metric: lbls}
|
||||
if r.compareOp() == ruletypes.ValueIsAbove {
|
||||
alertSmpl = Sample{Point: Point{V: series.Points[len(series.Points)-1].Value}, Metric: lbls}
|
||||
if r.compareOp() == ValueIsAbove {
|
||||
if series.Points[len(series.Points)-1].Value > r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsBelow {
|
||||
} else if r.compareOp() == ValueIsBelow {
|
||||
if series.Points[len(series.Points)-1].Value < r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsEq {
|
||||
} else if r.compareOp() == ValueIsEq {
|
||||
if series.Points[len(series.Points)-1].Value == r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
} else if r.compareOp() == ruletypes.ValueIsNotEq {
|
||||
} else if r.compareOp() == ValueIsNotEq {
|
||||
if series.Points[len(series.Points)-1].Value != r.targetVal() {
|
||||
shouldAlert = true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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) {
|
||||
@@ -18,7 +17,7 @@ func TestBaseRule_RequireMinPoints(t *testing.T) {
|
||||
{
|
||||
name: "test should skip if less than min points",
|
||||
rule: &BaseRule{
|
||||
ruleCondition: &ruletypes.RuleCondition{
|
||||
ruleCondition: &RuleCondition{
|
||||
RequireMinPoints: true,
|
||||
RequiredNumPoints: 4,
|
||||
},
|
||||
@@ -34,11 +33,11 @@ func TestBaseRule_RequireMinPoints(t *testing.T) {
|
||||
{
|
||||
name: "test should alert if more than min points",
|
||||
rule: &BaseRule{
|
||||
ruleCondition: &ruletypes.RuleCondition{
|
||||
ruleCondition: &RuleCondition{
|
||||
RequireMinPoints: true,
|
||||
RequiredNumPoints: 4,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOp: ValueIsAbove,
|
||||
MatchType: AtleastOnce,
|
||||
Target: &threshold,
|
||||
},
|
||||
},
|
||||
|
||||
387
pkg/query-service/rules/db.go
Normal file
387
pkg/query-service/rules/db.go
Normal file
@@ -0,0 +1,387 @@
|
||||
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
|
||||
}
|
||||
@@ -1,71 +1,237 @@
|
||||
package ruletypes
|
||||
package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeInvalidPlannedMaintenancePayload = errors.MustNewCode("invalid_planned_maintenance_payload")
|
||||
ErrMissingName = errors.New("missing name")
|
||||
ErrMissingSchedule = errors.New("missing schedule")
|
||||
ErrMissingTimezone = errors.New("missing timezone")
|
||||
ErrMissingRepeatType = errors.New("missing repeat type")
|
||||
ErrMissingDuration = errors.New("missing duration")
|
||||
)
|
||||
|
||||
type StorablePlannedMaintenance struct {
|
||||
bun.BaseModel `bun:"table:planned_maintenance"`
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
Description string `bun:"description,type:text"`
|
||||
Schedule *Schedule `bun:"schedule,type:text,notnull"`
|
||||
OrgID string `bun:"org_id,type:text"`
|
||||
}
|
||||
|
||||
type GettablePlannedMaintenance struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Schedule *Schedule `json:"schedule"`
|
||||
RuleIDs []string `json:"alertIds"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
type PlannedMaintenance struct {
|
||||
Id int64 `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Schedule *Schedule `json:"schedule" db:"schedule"`
|
||||
AlertIds *AlertIds `json:"alertIds" db:"alert_ids"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
CreatedBy string `json:"createdBy" db:"created_by"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
UpdatedBy string `json:"updatedBy" db:"updated_by"`
|
||||
Status string `json:"status"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
|
||||
type StorablePlannedMaintenanceRule struct {
|
||||
bun.BaseModel `bun:"table:planned_maintenance_rule"`
|
||||
types.Identifiable
|
||||
PlannedMaintenanceID valuer.UUID `bun:"planned_maintenance_id,type:text"`
|
||||
RuleID valuer.UUID `bun:"rule_id,type:text"`
|
||||
type AlertIds []string
|
||||
|
||||
func (a *AlertIds) Scan(src interface{}) error {
|
||||
if data, ok := src.([]byte); ok {
|
||||
return json.Unmarshal(data, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GettablePlannedMaintenanceRule struct {
|
||||
*StorablePlannedMaintenance `bun:",extend"`
|
||||
Rules []*StorablePlannedMaintenanceRule `bun:"rel:has-many,join:id=planned_maintenance_id"`
|
||||
func (a *AlertIds) Value() (driver.Value, error) {
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
|
||||
type Schedule struct {
|
||||
Timezone string `json:"timezone"`
|
||||
StartTime time.Time `json:"startTime,omitempty"`
|
||||
EndTime time.Time `json:"endTime,omitempty"`
|
||||
Recurrence *Recurrence `json:"recurrence"`
|
||||
}
|
||||
|
||||
func (s *Schedule) Scan(src interface{}) error {
|
||||
if data, ok := src.([]byte); ok {
|
||||
return json.Unmarshal(data, s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Schedule) Value() (driver.Value, error) {
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
type RepeatType string
|
||||
|
||||
const (
|
||||
RepeatTypeDaily RepeatType = "daily"
|
||||
RepeatTypeWeekly RepeatType = "weekly"
|
||||
RepeatTypeMonthly RepeatType = "monthly"
|
||||
)
|
||||
|
||||
type RepeatOn string
|
||||
|
||||
const (
|
||||
RepeatOnSunday RepeatOn = "sunday"
|
||||
RepeatOnMonday RepeatOn = "monday"
|
||||
RepeatOnTuesday RepeatOn = "tuesday"
|
||||
RepeatOnWednesday RepeatOn = "wednesday"
|
||||
RepeatOnThursday RepeatOn = "thursday"
|
||||
RepeatOnFriday RepeatOn = "friday"
|
||||
RepeatOnSaturday RepeatOn = "saturday"
|
||||
)
|
||||
|
||||
var RepeatOnAllMap = map[RepeatOn]time.Weekday{
|
||||
RepeatOnSunday: time.Sunday,
|
||||
RepeatOnMonday: time.Monday,
|
||||
RepeatOnTuesday: time.Tuesday,
|
||||
RepeatOnWednesday: time.Wednesday,
|
||||
RepeatOnThursday: time.Thursday,
|
||||
RepeatOnFriday: time.Friday,
|
||||
RepeatOnSaturday: time.Saturday,
|
||||
}
|
||||
|
||||
type Recurrence struct {
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime *time.Time `json:"endTime,omitempty"`
|
||||
Duration Duration `json:"duration"`
|
||||
RepeatType RepeatType `json:"repeatType"`
|
||||
RepeatOn []RepeatOn `json:"repeatOn"`
|
||||
}
|
||||
|
||||
func (r *Recurrence) Scan(src interface{}) error {
|
||||
if data, ok := src.([]byte); ok {
|
||||
return json.Unmarshal(data, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Recurrence) Value() (driver.Value, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
func (s Schedule) MarshalJSON() ([]byte, error) {
|
||||
loc, err := time.LoadLocation(s.Timezone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var startTime, endTime time.Time
|
||||
if !s.StartTime.IsZero() {
|
||||
startTime = time.Date(s.StartTime.Year(), s.StartTime.Month(), s.StartTime.Day(), s.StartTime.Hour(), s.StartTime.Minute(), s.StartTime.Second(), s.StartTime.Nanosecond(), loc)
|
||||
}
|
||||
if !s.EndTime.IsZero() {
|
||||
endTime = time.Date(s.EndTime.Year(), s.EndTime.Month(), s.EndTime.Day(), s.EndTime.Hour(), s.EndTime.Minute(), s.EndTime.Second(), s.EndTime.Nanosecond(), loc)
|
||||
}
|
||||
|
||||
var recurrence *Recurrence
|
||||
if s.Recurrence != nil {
|
||||
recStartTime := time.Date(s.Recurrence.StartTime.Year(), s.Recurrence.StartTime.Month(), s.Recurrence.StartTime.Day(), s.Recurrence.StartTime.Hour(), s.Recurrence.StartTime.Minute(), s.Recurrence.StartTime.Second(), s.Recurrence.StartTime.Nanosecond(), loc)
|
||||
var recEndTime *time.Time
|
||||
if s.Recurrence.EndTime != nil {
|
||||
end := time.Date(s.Recurrence.EndTime.Year(), s.Recurrence.EndTime.Month(), s.Recurrence.EndTime.Day(), s.Recurrence.EndTime.Hour(), s.Recurrence.EndTime.Minute(), s.Recurrence.EndTime.Second(), s.Recurrence.EndTime.Nanosecond(), loc)
|
||||
recEndTime = &end
|
||||
}
|
||||
recurrence = &Recurrence{
|
||||
StartTime: recStartTime,
|
||||
EndTime: recEndTime,
|
||||
Duration: s.Recurrence.Duration,
|
||||
RepeatType: s.Recurrence.RepeatType,
|
||||
RepeatOn: s.Recurrence.RepeatOn,
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(&struct {
|
||||
Timezone string `json:"timezone"`
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
Recurrence *Recurrence `json:"recurrence,omitempty"`
|
||||
}{
|
||||
Timezone: s.Timezone,
|
||||
StartTime: startTime.Format(time.RFC3339),
|
||||
EndTime: endTime.Format(time.RFC3339),
|
||||
Recurrence: recurrence,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Schedule) UnmarshalJSON(data []byte) error {
|
||||
aux := &struct {
|
||||
Timezone string `json:"timezone"`
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
Recurrence *Recurrence `json:"recurrence,omitempty"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loc, err := time.LoadLocation(aux.Timezone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var startTime time.Time
|
||||
if aux.StartTime != "" {
|
||||
startTime, err = time.Parse(time.RFC3339, aux.StartTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.StartTime = time.Date(startTime.Year(), startTime.Month(), startTime.Day(), startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(), loc)
|
||||
}
|
||||
|
||||
var endTime time.Time
|
||||
if aux.EndTime != "" {
|
||||
endTime, err = time.Parse(time.RFC3339, aux.EndTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.EndTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), endTime.Hour(), endTime.Minute(), endTime.Second(), endTime.Nanosecond(), loc)
|
||||
}
|
||||
|
||||
s.Timezone = aux.Timezone
|
||||
|
||||
if aux.Recurrence != nil {
|
||||
recStartTime, err := time.Parse(time.RFC3339, aux.Recurrence.StartTime.Format(time.RFC3339))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var recEndTime *time.Time
|
||||
if aux.Recurrence.EndTime != nil {
|
||||
end, err := time.Parse(time.RFC3339, aux.Recurrence.EndTime.Format(time.RFC3339))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endConverted := time.Date(end.Year(), end.Month(), end.Day(), end.Hour(), end.Minute(), end.Second(), end.Nanosecond(), loc)
|
||||
recEndTime = &endConverted
|
||||
}
|
||||
|
||||
s.Recurrence = &Recurrence{
|
||||
StartTime: time.Date(recStartTime.Year(), recStartTime.Month(), recStartTime.Day(), recStartTime.Hour(), recStartTime.Minute(), recStartTime.Second(), recStartTime.Nanosecond(), loc),
|
||||
EndTime: recEndTime,
|
||||
Duration: aux.Recurrence.Duration,
|
||||
RepeatType: aux.Recurrence.RepeatType,
|
||||
RepeatOn: aux.Recurrence.RepeatOn,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PlannedMaintenance) shouldSkip(ruleID string, now time.Time) bool {
|
||||
// Check if the alert ID is in the maintenance window
|
||||
found := false
|
||||
if len(m.RuleIDs) > 0 {
|
||||
for _, alertID := range m.RuleIDs {
|
||||
if m.AlertIds != nil {
|
||||
for _, alertID := range *m.AlertIds {
|
||||
if alertID == ruleID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no alert ids, then skip all alerts
|
||||
if len(m.RuleIDs) == 0 {
|
||||
if m.AlertIds == nil || len(*m.AlertIds) == 0 {
|
||||
found = true
|
||||
}
|
||||
|
||||
@@ -74,6 +240,7 @@ func (m *GettablePlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bo
|
||||
}
|
||||
|
||||
zap.L().Info("alert found in maintenance", zap.String("alert", ruleID), zap.String("maintenance", m.Name))
|
||||
|
||||
// If alert is found, we check if it should be skipped based on the schedule
|
||||
loc, err := time.LoadLocation(m.Schedule.Timezone)
|
||||
if err != nil {
|
||||
@@ -145,7 +312,7 @@ func (m *GettablePlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bo
|
||||
|
||||
// checkDaily rebases the recurrence start to today (or yesterday if needed)
|
||||
// and returns true if currentTime is within [candidate, candidate+Duration].
|
||||
func (m *GettablePlannedMaintenance) checkDaily(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
func (m *PlannedMaintenance) checkDaily(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
candidate := time.Date(
|
||||
currentTime.Year(), currentTime.Month(), currentTime.Day(),
|
||||
rec.StartTime.Hour(), rec.StartTime.Minute(), 0, 0,
|
||||
@@ -160,7 +327,7 @@ func (m *GettablePlannedMaintenance) checkDaily(currentTime time.Time, rec *Recu
|
||||
// checkWeekly finds the most recent allowed occurrence by rebasing the recurrence’s
|
||||
// time-of-day onto the allowed weekday. It does this for each allowed day and returns true
|
||||
// if the current time falls within the candidate window.
|
||||
func (m *GettablePlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
func (m *PlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
// If no days specified, treat as every day (like daily).
|
||||
if len(rec.RepeatOn) == 0 {
|
||||
return m.checkDaily(currentTime, rec, loc)
|
||||
@@ -192,7 +359,7 @@ func (m *GettablePlannedMaintenance) checkWeekly(currentTime time.Time, rec *Rec
|
||||
|
||||
// checkMonthly rebases the candidate occurrence using the recurrence's day-of-month.
|
||||
// If the candidate for the current month is in the future, it uses the previous month.
|
||||
func (m *GettablePlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
func (m *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
refDay := rec.StartTime.Day()
|
||||
year, month, _ := currentTime.Date()
|
||||
lastDay := time.Date(year, month+1, 0, 0, 0, 0, 0, loc).Day()
|
||||
@@ -224,15 +391,15 @@ func (m *GettablePlannedMaintenance) checkMonthly(currentTime time.Time, rec *Re
|
||||
return currentTime.Sub(candidate) <= time.Duration(rec.Duration)
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) IsActive(now time.Time) bool {
|
||||
func (m *PlannedMaintenance) IsActive(now time.Time) bool {
|
||||
ruleID := "maintenance"
|
||||
if len(m.RuleIDs) > 0 {
|
||||
ruleID = (m.RuleIDs)[0]
|
||||
if m.AlertIds != nil && len(*m.AlertIds) > 0 {
|
||||
ruleID = (*m.AlertIds)[0]
|
||||
}
|
||||
return m.ShouldSkip(ruleID, now)
|
||||
return m.shouldSkip(ruleID, now)
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) IsUpcoming() bool {
|
||||
func (m *PlannedMaintenance) IsUpcoming() bool {
|
||||
loc, err := time.LoadLocation(m.Schedule.Timezone)
|
||||
if err != nil {
|
||||
// handle error appropriately, for example log and return false or fallback to UTC
|
||||
@@ -250,47 +417,47 @@ func (m *GettablePlannedMaintenance) IsUpcoming() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) IsRecurring() bool {
|
||||
func (m *PlannedMaintenance) IsRecurring() bool {
|
||||
return m.Schedule.Recurrence != nil
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) Validate() error {
|
||||
func (m *PlannedMaintenance) Validate() error {
|
||||
if m.Name == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing name in the payload")
|
||||
return ErrMissingName
|
||||
}
|
||||
if m.Schedule == nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing schedule in the payload")
|
||||
return ErrMissingSchedule
|
||||
}
|
||||
if m.Schedule.Timezone == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing timezone in the payload")
|
||||
return ErrMissingTimezone
|
||||
}
|
||||
|
||||
_, err := time.LoadLocation(m.Schedule.Timezone)
|
||||
if err != nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "invalid timezone in the payload")
|
||||
return errors.New("invalid timezone")
|
||||
}
|
||||
|
||||
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
|
||||
if m.Schedule.StartTime.After(m.Schedule.EndTime) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
|
||||
return errors.New("start time cannot be after end time")
|
||||
}
|
||||
}
|
||||
|
||||
if m.Schedule.Recurrence != nil {
|
||||
if m.Schedule.Recurrence.RepeatType == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing repeat type in the payload")
|
||||
return ErrMissingRepeatType
|
||||
}
|
||||
if m.Schedule.Recurrence.Duration == 0 {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing duration in the payload")
|
||||
return ErrMissingDuration
|
||||
}
|
||||
if m.Schedule.Recurrence.EndTime != nil && m.Schedule.Recurrence.EndTime.Before(m.Schedule.Recurrence.StartTime) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "end time cannot be before start time")
|
||||
return errors.New("end time cannot be before start time")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m GettablePlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
func (m PlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
now := time.Now().In(time.FixedZone(m.Schedule.Timezone, 0))
|
||||
var status string
|
||||
if m.IsActive(now) {
|
||||
@@ -309,11 +476,11 @@ func (m GettablePlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
return json.Marshal(struct {
|
||||
Id string `json:"id" db:"id"`
|
||||
Id int64 `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Schedule *Schedule `json:"schedule" db:"schedule"`
|
||||
AlertIds []string `json:"alertIds" db:"alert_ids"`
|
||||
AlertIds *AlertIds `json:"alertIds" db:"alert_ids"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
CreatedBy string `json:"createdBy" db:"created_by"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
@@ -325,7 +492,7 @@ func (m GettablePlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Schedule: m.Schedule,
|
||||
AlertIds: m.RuleIDs,
|
||||
AlertIds: m.AlertIds,
|
||||
CreatedAt: m.CreatedAt,
|
||||
CreatedBy: m.CreatedBy,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
@@ -334,32 +501,3 @@ func (m GettablePlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
Kind: kind,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenanceRule) ConvertGettableMaintenanceRuleToGettableMaintenance() *GettablePlannedMaintenance {
|
||||
ruleIDs := []string{}
|
||||
if m.Rules != nil {
|
||||
for _, storableMaintenanceRule := range m.Rules {
|
||||
ruleIDs = append(ruleIDs, storableMaintenanceRule.RuleID.StringValue())
|
||||
}
|
||||
}
|
||||
|
||||
return &GettablePlannedMaintenance{
|
||||
Id: m.ID.StringValue(),
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Schedule: m.Schedule,
|
||||
RuleIDs: ruleIDs,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
CreatedBy: m.CreatedBy,
|
||||
UpdatedBy: m.UpdatedBy,
|
||||
}
|
||||
}
|
||||
|
||||
type MaintenanceStore interface {
|
||||
CreatePlannedMaintenance(context.Context, GettablePlannedMaintenance) (valuer.UUID, error)
|
||||
DeletePlannedMaintenance(context.Context, valuer.UUID) error
|
||||
GetPlannedMaintenanceByID(context.Context, valuer.UUID) (*GettablePlannedMaintenance, error)
|
||||
EditPlannedMaintenance(context.Context, GettablePlannedMaintenance, valuer.UUID) error
|
||||
GetAllPlannedMaintenance(context.Context, string) ([]*GettablePlannedMaintenance, error)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ruletypes
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -14,13 +14,13 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
maintenance *GettablePlannedMaintenance
|
||||
maintenance *PlannedMaintenance
|
||||
ts time.Time
|
||||
skip bool
|
||||
}{
|
||||
{
|
||||
name: "only-on-saturday",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "Europe/London",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -37,7 +37,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Testing weekly recurrence with midnight crossing
|
||||
{
|
||||
name: "weekly-across-midnight-previous-day",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -54,7 +54,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Testing weekly recurrence with midnight crossing
|
||||
{
|
||||
name: "weekly-across-midnight-previous-day",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -71,7 +71,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Testing weekly recurrence with multi day duration
|
||||
{
|
||||
name: "weekly-across-midnight-previous-day",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -88,7 +88,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Weekly recurrence where the previous day is not in RepeatOn
|
||||
{
|
||||
name: "weekly-across-midnight-previous-day-not-in-repeaton",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -105,7 +105,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Daily recurrence with midnight crossing
|
||||
{
|
||||
name: "daily-maintenance-across-midnight",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -121,7 +121,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Exactly at start time boundary
|
||||
{
|
||||
name: "at-start-time-boundary",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -137,7 +137,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Exactly at end time boundary
|
||||
{
|
||||
name: "at-end-time-boundary",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -153,7 +153,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Monthly maintenance with multi-day duration
|
||||
{
|
||||
name: "monthly-multi-day-duration",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -169,7 +169,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Weekly maintenance with multi-day duration
|
||||
{
|
||||
name: "weekly-multi-day-duration",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -186,7 +186,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Monthly maintenance that crosses to next month
|
||||
{
|
||||
name: "monthly-crosses-to-next-month",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -202,7 +202,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Different timezone tests
|
||||
{
|
||||
name: "timezone-offset-test",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "America/New_York", // UTC-5 or UTC-4 depending on DST
|
||||
Recurrence: &Recurrence{
|
||||
@@ -218,7 +218,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Test negative case - time well outside window
|
||||
{
|
||||
name: "daily-maintenance-time-outside-window",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -234,7 +234,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Test for recurring maintenance with an end date that is before the current time
|
||||
{
|
||||
name: "recurring-maintenance-with-past-end-date",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -251,7 +251,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Monthly recurring maintenance spanning end of month into beginning of next month
|
||||
{
|
||||
name: "monthly-maintenance-spans-month-end",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -267,7 +267,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Test for RepeatOn with empty array (should apply to all days)
|
||||
{
|
||||
name: "weekly-empty-repeaton",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -284,7 +284,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// February has fewer days than January - test the edge case when maintenance is on 31st
|
||||
{
|
||||
name: "monthly-maintenance-february-fewer-days",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -299,7 +299,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "daily-maintenance-crosses-midnight",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -314,7 +314,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "monthly-maintenance-crosses-month-end",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -329,7 +329,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -344,7 +344,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "weekly-maintenance-crosses-midnight",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -360,7 +360,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -375,7 +375,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "daily-maintenance-crosses-midnight",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -390,7 +390,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-hours",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -405,7 +405,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fixed planned maintenance start <= ts <= end",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
StartTime: time.Now().UTC().Add(-time.Hour),
|
||||
@@ -417,7 +417,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fixed planned maintenance start >= ts",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
StartTime: time.Now().UTC().Add(time.Hour),
|
||||
@@ -429,7 +429,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fixed planned maintenance ts < start",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
StartTime: time.Now().UTC().Add(time.Hour),
|
||||
@@ -441,7 +441,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat sunday, saturday, weekly for 24 hours, in Us/Eastern timezone",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "US/Eastern",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -457,7 +457,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -472,7 +472,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -487,7 +487,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -502,7 +502,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -518,7 +518,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -534,7 +534,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -550,7 +550,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -566,7 +566,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -582,7 +582,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -597,7 +597,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -612,7 +612,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -628,7 +628,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
}
|
||||
|
||||
for idx, c := range cases {
|
||||
result := c.maintenance.ShouldSkip(c.name, c.ts)
|
||||
result := c.maintenance.shouldSkip(c.name, c.ts)
|
||||
if result != c.skip {
|
||||
t.Errorf("skip %v, got %v, case:%d - %s", c.skip, result, idx, c.name)
|
||||
}
|
||||
@@ -23,21 +23,16 @@ 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 *ruletypes.PostableRule
|
||||
Rule *PostableRule
|
||||
TaskName string
|
||||
RuleStore ruletypes.RuleStore
|
||||
MaintenanceStore ruletypes.MaintenanceStore
|
||||
RuleDB RuleDB
|
||||
Logger *zap.Logger
|
||||
Reader interfaces.Reader
|
||||
Cache cache.Cache
|
||||
@@ -46,13 +41,11 @@ type PrepareTaskOptions struct {
|
||||
SQLStore sqlstore.SQLStore
|
||||
UseLogsNewSchema bool
|
||||
UseTraceNewSchema bool
|
||||
OrgID string
|
||||
}
|
||||
|
||||
type PrepareTestRuleOptions struct {
|
||||
Rule *ruletypes.PostableRule
|
||||
RuleStore ruletypes.RuleStore
|
||||
MaintenanceStore ruletypes.MaintenanceStore
|
||||
Rule *PostableRule
|
||||
RuleDB RuleDB
|
||||
Logger *zap.Logger
|
||||
Reader interfaces.Reader
|
||||
Cache cache.Cache
|
||||
@@ -116,8 +109,7 @@ type Manager struct {
|
||||
mtx sync.RWMutex
|
||||
block chan struct{}
|
||||
// datastore to store alert definitions
|
||||
ruleStore ruletypes.RuleStore
|
||||
maintenanceStore ruletypes.MaintenanceStore
|
||||
ruleDB RuleDB
|
||||
|
||||
logger *zap.Logger
|
||||
reader interfaces.Reader
|
||||
@@ -154,7 +146,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
|
||||
var task Task
|
||||
|
||||
ruleId := RuleIdFromTaskName(opts.TaskName)
|
||||
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
if opts.Rule.RuleType == RuleTypeThreshold {
|
||||
// create a threshold rule
|
||||
tr, err := NewThresholdRule(
|
||||
ruleId,
|
||||
@@ -173,9 +165,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.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(TaskTypeCh, opts.TaskName, taskNamesuffix, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
|
||||
} else if opts.Rule.RuleType == RuleTypeProm {
|
||||
|
||||
// create promql rule
|
||||
pr, err := NewPromRule(
|
||||
@@ -194,10 +186,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.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(TaskTypeProm, opts.TaskName, taskNamesuffix, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, RuleTypeProm, RuleTypeThreshold)
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -207,15 +199,12 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
|
||||
// by calling the Run method.
|
||||
func NewManager(o *ManagerOptions) (*Manager, error) {
|
||||
o = defaultOptions(o)
|
||||
ruleStore := sqlrulestore.NewRuleStore(o.DBConn, o.SQLStore)
|
||||
maintenanceStore := sqlrulestore.NewMaintenanceStore(o.SQLStore)
|
||||
|
||||
telemetry.GetInstance().SetAlertsInfoCallback(ruleStore.GetAlertsInfo)
|
||||
db := NewRuleDB(o.DBConn, o.SQLStore)
|
||||
telemetry.GetInstance().SetAlertsInfoCallback(db.GetAlertsInfo)
|
||||
m := &Manager{
|
||||
tasks: map[string]Task{},
|
||||
rules: map[string]Rule{},
|
||||
ruleStore: ruleStore,
|
||||
maintenanceStore: maintenanceStore,
|
||||
ruleDB: db,
|
||||
opts: o,
|
||||
block: make(chan struct{}),
|
||||
logger: o.Logger,
|
||||
@@ -230,19 +219,15 @@ func NewManager(o *ManagerOptions) (*Manager, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Start(ctx context.Context) {
|
||||
if err := m.initiate(ctx); err != nil {
|
||||
func (m *Manager) Start() {
|
||||
if err := m.initiate(); err != nil {
|
||||
zap.L().Error("failed to initialize alerting rules manager", zap.Error(err))
|
||||
}
|
||||
m.run(ctx)
|
||||
m.run()
|
||||
}
|
||||
|
||||
func (m *Manager) RuleStore() ruletypes.RuleStore {
|
||||
return m.ruleStore
|
||||
}
|
||||
|
||||
func (m *Manager) MaintenanceStore() ruletypes.MaintenanceStore {
|
||||
return m.maintenanceStore
|
||||
func (m *Manager) RuleDB() RuleDB {
|
||||
return m.ruleDB
|
||||
}
|
||||
|
||||
func (m *Manager) Pause(b bool) {
|
||||
@@ -253,51 +238,44 @@ func (m *Manager) Pause(b bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) initiate(ctx context.Context) error {
|
||||
orgIDs, err := m.ruleStore.ListOrgs(ctx)
|
||||
func (m *Manager) initiate() error {
|
||||
storedRules, err := m.ruleDB.GetStoredRules(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(storedRules) == 0 {
|
||||
return nil
|
||||
}
|
||||
var loadErrors []error
|
||||
for _, orgID := range orgIDs {
|
||||
storedRules, err := m.ruleStore.GetStoredRules(ctx, orgID)
|
||||
|
||||
for _, rec := range storedRules {
|
||||
taskName := fmt.Sprintf("%d-groupname", rec.Id)
|
||||
parsedRule, err := ParsePostableRule([]byte(rec.Data))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(storedRules) == 0 {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, ErrFailedToParseJSON) {
|
||||
zap.L().Info("failed to load rule in json format, trying yaml now:", zap.String("name", taskName))
|
||||
|
||||
for _, rec := range storedRules {
|
||||
taskName := fmt.Sprintf("%s-groupname", rec.ID.StringValue())
|
||||
parsedRule, err := ruletypes.ParsePostableRule([]byte(rec.Data))
|
||||
// see if rule is stored in yaml format
|
||||
parsedRule, err = parsePostableRule([]byte(rec.Data), RuleDataKindYaml)
|
||||
|
||||
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))
|
||||
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
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,13 +288,13 @@ func (m *Manager) initiate(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Run starts processing of the rule manager.
|
||||
func (m *Manager) run(_ context.Context) {
|
||||
func (m *Manager) run() {
|
||||
// initiate blocked tasks
|
||||
close(m.block)
|
||||
}
|
||||
|
||||
// Stop the rule manager's rule evaluation cycles.
|
||||
func (m *Manager) Stop(ctx context.Context) {
|
||||
func (m *Manager) Stop() {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
|
||||
@@ -331,41 +309,28 @@ func (m *Manager) Stop(ctx context.Context) {
|
||||
|
||||
// EditRuleDefinition writes the rule definition to the
|
||||
// datastore and also updates the rule executor
|
||||
func (m *Manager) EditRule(ctx context.Context, ruleStr string, idStr string) error {
|
||||
func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error {
|
||||
claims, ok := authtypes.ClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return errors.New("claims not found in context")
|
||||
}
|
||||
|
||||
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))
|
||||
parsedRule, err := ParsePostableRule([]byte(ruleStr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingRule, err := m.ruleStore.GetStoredRule(ctx, ruleUUID)
|
||||
existingRule, err := m.ruleDB.GetStoredRule(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingRule.UpdatedAt = time.Now()
|
||||
existingRule.UpdatedBy = claims.Email
|
||||
now := time.Now()
|
||||
existingRule.UpdatedAt = &now
|
||||
existingRule.UpdatedBy = &claims.Email
|
||||
existingRule.Data = ruleStr
|
||||
|
||||
return m.ruleStore.EditRule(ctx, existingRule, func(ctx context.Context) error {
|
||||
return m.ruleDB.EditRule(ctx, existingRule, func(ctx context.Context) error {
|
||||
cfg, err := m.alertmanager.GetConfig(ctx, claims.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -385,7 +350,7 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, idStr string) er
|
||||
preferredChannels = parsedRule.PreferredChannels
|
||||
}
|
||||
|
||||
err = cfg.UpdateRuleIDMatcher(ruleUUID.StringValue(), preferredChannels)
|
||||
err = cfg.UpdateRuleIDMatcher(id, preferredChannels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -396,7 +361,7 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, idStr string) er
|
||||
}
|
||||
|
||||
if !m.opts.DisableRules {
|
||||
err = m.syncRuleStateWithTask(ctx, claims.OrgID, prepareTaskName(existingRule.ID.StringValue()), parsedRule)
|
||||
err = m.syncRuleStateWithTask(prepareTaskName(existingRule.Id), parsedRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -406,26 +371,25 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, idStr string) er
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) editTask(_ context.Context, orgID string, rule *ruletypes.PostableRule, taskName string) error {
|
||||
func (m *Manager) editTask(rule *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,
|
||||
RuleStore: m.ruleStore,
|
||||
MaintenanceStore: m.maintenanceStore,
|
||||
Logger: m.logger,
|
||||
Reader: m.reader,
|
||||
Cache: m.cache,
|
||||
ManagerOpts: m.opts,
|
||||
NotifyFunc: m.prepareNotifyFunc(),
|
||||
SQLStore: m.sqlstore,
|
||||
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,
|
||||
|
||||
UseLogsNewSchema: m.opts.UseLogsNewSchema,
|
||||
UseTraceNewSchema: m.opts.UseTraceNewSchema,
|
||||
OrgID: orgID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -462,11 +426,11 @@ func (m *Manager) editTask(_ context.Context, orgID string, rule *ruletypes.Post
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteRule(ctx context.Context, idStr string) error {
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
func (m *Manager) DeleteRule(ctx context.Context, id string) error {
|
||||
_, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
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")
|
||||
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")
|
||||
}
|
||||
|
||||
claims, ok := authtypes.ClaimsFromContext(ctx)
|
||||
@@ -474,18 +438,18 @@ func (m *Manager) DeleteRule(ctx context.Context, idStr string) error {
|
||||
return errors.New("claims not found in context")
|
||||
}
|
||||
|
||||
_, err = m.ruleStore.GetStoredRule(ctx, id)
|
||||
_, err = m.ruleDB.GetStoredRule(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.ruleStore.DeleteRule(ctx, id, func(ctx context.Context) error {
|
||||
return m.ruleDB.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.StringValue())
|
||||
err = cfg.DeleteRuleIDMatcher(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -495,7 +459,7 @@ func (m *Manager) DeleteRule(ctx context.Context, idStr string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
taskName := prepareTaskName(id.StringValue())
|
||||
taskName := prepareTaskName(id)
|
||||
if !m.opts.DisableRules {
|
||||
m.deleteTask(taskName)
|
||||
}
|
||||
@@ -522,35 +486,27 @@ 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) (*ruletypes.GettableRule, error) {
|
||||
func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule, error) {
|
||||
parsedRule, err := ParsePostableRule([]byte(ruleStr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := authtypes.ClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("claims not found in context")
|
||||
}
|
||||
|
||||
parsedRule, err := ruletypes.ParsePostableRule([]byte(ruleStr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
storedRule := &StoredRule{
|
||||
CreatedAt: &now,
|
||||
CreatedBy: &claims.Email,
|
||||
UpdatedAt: &now,
|
||||
UpdatedBy: &claims.Email,
|
||||
Data: ruleStr,
|
||||
}
|
||||
|
||||
id, err := m.ruleStore.CreateRule(ctx, storedRule, func(ctx context.Context, id valuer.UUID) error {
|
||||
id, err := m.ruleDB.CreateRule(ctx, storedRule, func(ctx context.Context, id int64) error {
|
||||
cfg, err := m.alertmanager.GetConfig(ctx, claims.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -570,7 +526,7 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*ruletypes.Ge
|
||||
preferredChannels = parsedRule.PreferredChannels
|
||||
}
|
||||
|
||||
err = cfg.CreateRuleIDMatcher(id.StringValue(), preferredChannels)
|
||||
err = cfg.CreateRuleIDMatcher(fmt.Sprintf("%d", id), preferredChannels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -580,9 +536,9 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*ruletypes.Ge
|
||||
return err
|
||||
}
|
||||
|
||||
taskName := prepareTaskName(id.StringValue())
|
||||
taskName := prepareTaskName(id)
|
||||
if !m.opts.DisableRules {
|
||||
if err := m.addTask(ctx, claims.OrgID, parsedRule, taskName); err != nil {
|
||||
if err := m.addTask(parsedRule, taskName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -593,31 +549,30 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*ruletypes.Ge
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ruletypes.GettableRule{
|
||||
Id: id.StringValue(),
|
||||
return &GettableRule{
|
||||
Id: fmt.Sprintf("%d", id),
|
||||
PostableRule: *parsedRule,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) addTask(_ context.Context, orgID string, rule *ruletypes.PostableRule, taskName string) error {
|
||||
func (m *Manager) addTask(rule *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,
|
||||
RuleStore: m.ruleStore,
|
||||
MaintenanceStore: m.maintenanceStore,
|
||||
Logger: m.logger,
|
||||
Reader: m.reader,
|
||||
Cache: m.cache,
|
||||
ManagerOpts: m.opts,
|
||||
NotifyFunc: m.prepareNotifyFunc(),
|
||||
SQLStore: m.sqlstore,
|
||||
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,
|
||||
|
||||
UseLogsNewSchema: m.opts.UseLogsNewSchema,
|
||||
UseTraceNewSchema: m.opts.UseTraceNewSchema,
|
||||
OrgID: orgID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -693,17 +648,17 @@ func (m *Manager) Rules() []Rule {
|
||||
}
|
||||
|
||||
// TriggeredAlerts returns the list of the manager's rules.
|
||||
func (m *Manager) TriggeredAlerts() []*ruletypes.NamedAlert {
|
||||
func (m *Manager) TriggeredAlerts() []*NamedAlert {
|
||||
// m.mtx.RLock()
|
||||
// defer m.mtx.RUnlock()
|
||||
|
||||
namedAlerts := []*ruletypes.NamedAlert{}
|
||||
namedAlerts := []*NamedAlert{}
|
||||
|
||||
for _, r := range m.rules {
|
||||
active := r.ActiveAlerts()
|
||||
|
||||
for _, a := range active {
|
||||
awn := &ruletypes.NamedAlert{
|
||||
awn := &NamedAlert{
|
||||
Alert: a,
|
||||
Name: r.Name(),
|
||||
}
|
||||
@@ -715,11 +670,11 @@ func (m *Manager) TriggeredAlerts() []*ruletypes.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 ...*ruletypes.Alert)
|
||||
type NotifyFunc func(ctx context.Context, orgID string, expr string, alerts ...*Alert)
|
||||
|
||||
// prepareNotifyFunc implements the NotifyFunc for a Notifier.
|
||||
func (m *Manager) prepareNotifyFunc() NotifyFunc {
|
||||
return func(ctx context.Context, orgID string, expr string, alerts ...*ruletypes.Alert) {
|
||||
return func(ctx context.Context, orgID string, expr string, alerts ...*Alert) {
|
||||
var res []*alertmanagertypes.PostableAlert
|
||||
|
||||
for _, alert := range alerts {
|
||||
@@ -752,7 +707,7 @@ func (m *Manager) prepareNotifyFunc() NotifyFunc {
|
||||
}
|
||||
|
||||
func (m *Manager) prepareTestNotifyFunc() NotifyFunc {
|
||||
return func(ctx context.Context, orgID string, expr string, alerts ...*ruletypes.Alert) {
|
||||
return func(ctx context.Context, orgID string, expr string, alerts ...*Alert) {
|
||||
if len(alerts) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -803,29 +758,26 @@ func (m *Manager) ListActiveRules() ([]Rule, error) {
|
||||
return ruleList, nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
func (m *Manager) ListRuleStates(ctx context.Context) (*GettableRules, error) {
|
||||
|
||||
// fetch rules from DB
|
||||
storedRules, err := m.ruleStore.GetStoredRules(ctx, claims.OrgID)
|
||||
storedRules, err := m.ruleDB.GetStoredRules(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initiate response object
|
||||
resp := make([]*ruletypes.GettableRule, 0)
|
||||
resp := make([]*GettableRule, 0)
|
||||
|
||||
for _, s := range storedRules {
|
||||
|
||||
ruleResponse := &ruletypes.GettableRule{}
|
||||
ruleResponse := &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.String("id", s.ID.StringValue()), zap.Error(err))
|
||||
zap.L().Error("failed to unmarshal rule from db", zap.Int("id", s.Id), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
ruleResponse.Id = s.ID.StringValue()
|
||||
ruleResponse.Id = fmt.Sprintf("%d", s.Id)
|
||||
|
||||
// fetch state of rule from memory
|
||||
if rm, ok := m.rules[ruleResponse.Id]; !ok {
|
||||
@@ -834,40 +786,26 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules,
|
||||
} 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 &ruletypes.GettableRules{Rules: resp}, nil
|
||||
return &GettableRules{Rules: resp}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
func (m *Manager) GetRule(ctx context.Context, id string) (*GettableRule, error) {
|
||||
s, err := m.ruleDB.GetStoredRule(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := &ruletypes.GettableRule{}
|
||||
r := &GettableRule{}
|
||||
if err := json.Unmarshal([]byte(s.Data), r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Id = ruleUUID.StringValue()
|
||||
r.Id = fmt.Sprintf("%d", s.Id)
|
||||
// fetch state of rule from memory
|
||||
if rm, ok := m.rules[r.Id]; !ok {
|
||||
r.State = model.StateDisabled
|
||||
@@ -875,10 +813,10 @@ func (m *Manager) GetRule(ctx context.Context, idStr string) (*ruletypes.Gettabl
|
||||
} 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
|
||||
}
|
||||
@@ -886,7 +824,7 @@ func (m *Manager) GetRule(ctx context.Context, idStr string) (*ruletypes.Gettabl
|
||||
// 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(ctx context.Context, orgID string, taskName string, rule *ruletypes.PostableRule) error {
|
||||
func (m *Manager) syncRuleStateWithTask(taskName string, rule *PostableRule) error {
|
||||
|
||||
if rule.Disabled {
|
||||
// check if rule has any task running
|
||||
@@ -898,11 +836,11 @@ func (m *Manager) syncRuleStateWithTask(ctx context.Context, orgID string, taskN
|
||||
// check if rule has a task running
|
||||
if _, ok := m.tasks[taskName]; !ok {
|
||||
// rule has not task, start one
|
||||
if err := m.addTask(ctx, orgID, rule, taskName); err != nil {
|
||||
if err := m.addTask(rule, taskName); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := m.editTask(ctx, orgID, rule, taskName); err != nil {
|
||||
if err := m.editTask(rule, taskName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -917,41 +855,40 @@ func (m *Manager) syncRuleStateWithTask(ctx context.Context, orgID string, taskN
|
||||
// - 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, ruleIdStr string) (*ruletypes.GettableRule, error) {
|
||||
func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleId string) (*GettableRule, error) {
|
||||
claims, ok := authtypes.ClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("claims not found in context")
|
||||
}
|
||||
|
||||
ruleID, err := valuer.NewUUID(ruleIdStr)
|
||||
if err != nil {
|
||||
return nil, errors.New(err.Error())
|
||||
if ruleId == "" {
|
||||
return nil, fmt.Errorf("id is mandatory for patching rule")
|
||||
}
|
||||
|
||||
taskName := prepareTaskName(ruleID.StringValue())
|
||||
taskName := prepareTaskName(ruleId)
|
||||
|
||||
// retrieve rule from DB
|
||||
storedJSON, err := m.ruleStore.GetStoredRule(ctx, ruleID)
|
||||
storedJSON, err := m.ruleDB.GetStoredRule(ctx, ruleId)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to get stored rule with given id", zap.String("id", ruleID.StringValue()), zap.Error(err))
|
||||
zap.L().Error("failed to get stored rule with given id", zap.String("id", ruleId), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// storedRule holds the current stored rule from DB
|
||||
storedRule := ruletypes.PostableRule{}
|
||||
storedRule := 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.StringValue()), zap.Error(err))
|
||||
zap.L().Error("failed to unmarshal stored rule with given id", zap.String("id", ruleId), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// patchedRule is combo of stored rule and patch received in the request
|
||||
patchedRule, err := ruletypes.ParseIntoRule(storedRule, []byte(ruleStr), "json")
|
||||
patchedRule, err := 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(ctx, claims.OrgID, taskName, patchedRule); err != nil {
|
||||
if err := m.syncRuleStateWithTask(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
|
||||
}
|
||||
@@ -964,25 +901,25 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleIdStr strin
|
||||
|
||||
now := time.Now()
|
||||
storedJSON.Data = string(patchedRuleBytes)
|
||||
storedJSON.UpdatedBy = claims.Email
|
||||
storedJSON.UpdatedAt = now
|
||||
storedJSON.UpdatedBy = &claims.Email
|
||||
storedJSON.UpdatedAt = &now
|
||||
|
||||
err = m.ruleStore.EditRule(ctx, storedJSON, func(ctx context.Context) error { return nil })
|
||||
err = m.ruleDB.EditRule(ctx, storedJSON, func(ctx context.Context) error { return nil })
|
||||
if err != nil {
|
||||
if err := m.syncRuleStateWithTask(ctx, claims.OrgID, taskName, &storedRule); err != nil {
|
||||
if err := m.syncRuleStateWithTask(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 := ruletypes.GettableRule{
|
||||
Id: ruleID.StringValue(),
|
||||
response := GettableRule{
|
||||
Id: ruleId,
|
||||
PostableRule: *patchedRule,
|
||||
}
|
||||
|
||||
// fetch state of rule from memory
|
||||
if rm, ok := m.rules[ruleID.StringValue()]; !ok {
|
||||
if rm, ok := m.rules[ruleId]; !ok {
|
||||
response.State = model.StateDisabled
|
||||
response.Disabled = true
|
||||
} else {
|
||||
@@ -996,7 +933,7 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleIdStr strin
|
||||
// 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 := ruletypes.ParsePostableRule([]byte(ruleStr))
|
||||
parsedRule, err := ParsePostableRule([]byte(ruleStr))
|
||||
|
||||
if err != nil {
|
||||
return 0, model.BadRequest(err)
|
||||
@@ -1004,8 +941,7 @@ func (m *Manager) TestNotification(ctx context.Context, ruleStr string) (int, *m
|
||||
|
||||
alertCount, apiErr := m.prepareTestRuleFunc(PrepareTestRuleOptions{
|
||||
Rule: parsedRule,
|
||||
RuleStore: m.ruleStore,
|
||||
MaintenanceStore: m.maintenanceStore,
|
||||
RuleDB: m.ruleDB,
|
||||
Logger: m.logger,
|
||||
Reader: m.reader,
|
||||
Cache: m.cache,
|
||||
@@ -1019,37 +955,33 @@ 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][]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")}
|
||||
}
|
||||
func (m *Manager) GetAlertDetailsForMetricNames(ctx context.Context, metricNames []string) (map[string][]GettableRule, *model.ApiError) {
|
||||
result := make(map[string][]GettableRule)
|
||||
|
||||
result := make(map[string][]ruletypes.GettableRule)
|
||||
rules, err := m.ruleStore.GetStoredRules(ctx, claims.OrgID)
|
||||
rules, err := m.ruleDB.GetStoredRules(ctx)
|
||||
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][]ruletypes.GettableRule)
|
||||
metricRulesMap := make(map[string][]GettableRule)
|
||||
|
||||
for _, storedRule := range rules {
|
||||
var rule ruletypes.GettableRule
|
||||
var rule GettableRule
|
||||
if err := json.Unmarshal([]byte(storedRule.Data), &rule); err != nil {
|
||||
zap.L().Error("Invalid rule data", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
if rule.AlertType != ruletypes.AlertTypeMetric || rule.RuleCondition == nil || rule.RuleCondition.CompositeQuery == nil {
|
||||
if rule.AlertType != AlertTypeMetric || rule.RuleCondition == nil || rule.RuleCondition.CompositeQuery == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
rule.Id = storedRule.ID.StringValue()
|
||||
rule.CreatedAt = &storedRule.CreatedAt
|
||||
rule.CreatedBy = &storedRule.CreatedBy
|
||||
rule.UpdatedAt = &storedRule.UpdatedAt
|
||||
rule.UpdatedBy = &storedRule.UpdatedBy
|
||||
rule.Id = fmt.Sprintf("%d", storedRule.Id)
|
||||
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 != "" {
|
||||
@@ -1081,7 +1013,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([]ruletypes.GettableRule, 0)
|
||||
uniqueRules := make([]GettableRule, 0)
|
||||
|
||||
for _, rule := range rules {
|
||||
if !seen[rule.Id] {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user