Compare commits

..

23 Commits

Author SHA1 Message Date
aniket
44080a1d59 chore: resolved conflict 2025-11-02 11:09:13 +05:30
aniket
554c498209 Merge branch 'chore/json_formatter' of github.com:SigNoz/signoz into chore/filter-rules 2025-11-02 11:06:03 +05:30
aniket
156de83626 chore: minor changes 2025-11-01 22:16:25 +05:30
aniket
7c4a18687a chore: minor changes 2025-11-01 22:07:01 +05:30
aniket
7214f51e98 chore: minor changes 2025-11-01 21:06:58 +05:30
aniket
290e0754c6 chore: minor changes 2025-11-01 21:02:56 +05:30
aniket
0ea16f9472 chore: updated queries: 2025-11-01 16:52:33 +05:30
aniket
0d773211af chore: resolved pr comments 2025-11-01 16:10:40 +05:30
aniket
7028031e01 Merge branch 'chore/json_formatter' of github.com:SigNoz/signoz into chore/filter-rules 2025-11-01 16:07:18 +05:30
aniket
2a4407280d chore: resolved pr comments 2025-11-01 15:32:54 +05:30
Vikrant Gupta
8c75fb29a6 Merge branch 'main' into chore/json_formatter 2025-11-01 14:56:53 +05:30
aniket
85ea6105f8 chore: resolved pr comments 2025-11-01 14:55:42 +05:30
aniket
dc8fba6944 chore: resolved pr comments 2025-11-01 14:51:55 +05:30
Vishal Sharma
4b21c9d5f9 feat: add result count to data source search analytics event (#9444) 2025-10-31 12:35:24 +00:00
aniket
97bbc95aab Merge branch 'chore/json_formatter' of github.com:SigNoz/signoz into chore/filter-rules 2025-10-31 14:35:35 +05:30
aniket
fbcb17006d chore: added apend ident 2025-10-31 14:21:48 +05:30
aniket
d642b69f8e Merge branch 'main' of github.com:SigNoz/signoz into chore/filter-rules 2025-10-31 01:42:16 +05:30
aniket
7230069de6 chore: minor changes 2025-10-31 01:39:24 +05:30
aniket
80fff10273 chore: added keys, values api for rule filtering 2025-10-30 16:58:48 +05:30
aniket
39a6e3865e Merge branch 'chore/json_formatter' of github.com:SigNoz/signoz into chore/json_formatter 2025-10-30 16:53:32 +05:30
aniket
492e249c29 chore: updated json extract columns 2025-10-30 16:52:27 +05:30
aniketio-ctrl
d68affd1d6 Merge branch 'main' into chore/json_formatter 2025-10-27 17:34:32 +05:30
aniket
7bad6d5377 chore: added sql formatter for json 2025-10-27 17:14:29 +05:30
23 changed files with 1736 additions and 400 deletions

View File

@@ -0,0 +1,157 @@
package postgressqlstore
import (
"strings"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun/schema"
)
type formatter struct {
bunf schema.Formatter
}
func newFormatter(dialect schema.Dialect) sqlstore.SQLFormatter {
return &formatter{bunf: schema.NewFormatter(dialect)}
}
func (f *formatter) JSONExtractString(column, path string) []byte {
var sql []byte
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, f.convertJSONPathToPostgres(path)...)
return sql
}
func (f *formatter) JSONType(column, path string) []byte {
var sql []byte
sql = append(sql, "jsonb_typeof("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, f.convertJSONPathToPostgresWithMode(path, false)...)
sql = append(sql, ')')
return sql
}
func (f *formatter) JSONIsArray(column, path string) []byte {
var sql []byte
sql = append(sql, f.JSONType(column, path)...)
sql = append(sql, " = 'array'"...)
return sql
}
func (f *formatter) JSONArrayElements(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "jsonb_array_elements("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, f.convertJSONPathToPostgresWithMode(path, false)...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias)
}
func (f *formatter) JSONArrayOfStrings(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "jsonb_array_elements_text("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, f.convertJSONPathToPostgresWithMode(path, false)...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + "::text")
}
func (f *formatter) JSONKeys(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "jsonb_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, f.convertJSONPathToPostgresWithMode(path, false)...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".key")
}
func (f *formatter) JSONArrayAgg(expression string) []byte {
var sql []byte
sql = append(sql, "jsonb_agg("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}
func (f *formatter) JSONArrayLiteral(values ...string) []byte {
if len(values) == 0 {
return []byte("jsonb_build_array()")
}
var sql []byte
sql = append(sql, "jsonb_build_array("...)
for i, v := range values {
if i > 0 {
sql = append(sql, ", "...)
}
sql = append(sql, '\'')
sql = append(sql, v...)
sql = append(sql, '\'')
}
sql = append(sql, ')')
return sql
}
func (f *formatter) TextToJsonColumn(column string) []byte {
var sql []byte
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, "::jsonb"...)
return sql
}
func (f *formatter) convertJSONPathToPostgres(jsonPath string) string {
return f.convertJSONPathToPostgresWithMode(jsonPath, true)
}
func (f *formatter) convertJSONPathToPostgresWithMode(jsonPath string, asText bool) string {
path := strings.TrimPrefix(jsonPath, "$")
if path == "" || path == "." {
return ""
}
parts := strings.Split(strings.TrimPrefix(path, "."), ".")
if len(parts) == 0 {
return ""
}
var result strings.Builder
for i, part := range parts {
if i < len(parts)-1 {
result.WriteString("->")
result.WriteString("'")
result.WriteString(part)
result.WriteString("'")
} else {
if asText {
result.WriteString("->>")
} else {
result.WriteString("->")
}
result.WriteString("'")
result.WriteString(part)
result.WriteString("'")
}
}
return result.String()
}
func (f *formatter) LowerExpression(expression string) []byte {
var sql []byte
sql = append(sql, "lower("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}

View File

@@ -0,0 +1,488 @@
package postgressqlstore
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/uptrace/bun/dialect/pgdialect"
)
func TestJSONExtractString(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.field",
expected: `"data"->>'field'`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.name",
expected: `"metadata"->'user'->>'name'`,
},
{
name: "deeply nested path",
column: "json_col",
path: "$.level1.level2.level3",
expected: `"json_col"->'level1'->'level2'->>'level3'`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `"json_col"`,
},
{
name: "empty path",
column: "data",
path: "",
expected: `"data"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONExtractString(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONType(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.field",
expected: `jsonb_typeof("data"->'field')`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.age",
expected: `jsonb_typeof("metadata"->'user'->'age')`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `jsonb_typeof("json_col")`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONType(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONIsArray(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.items",
expected: `jsonb_typeof("data"->'items') = 'array'`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.tags",
expected: `jsonb_typeof("metadata"->'user'->'tags') = 'array'`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `jsonb_typeof("json_col") = 'array'`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONIsArray(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONArrayElements(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "elem",
expected: `jsonb_array_elements("data") AS "elem"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "elem",
expected: `jsonb_array_elements("data") AS "elem"`,
},
{
name: "nested path",
column: "metadata",
path: "$.items",
alias: "item",
expected: `jsonb_array_elements("metadata"->'items') AS "item"`,
},
{
name: "deeply nested path",
column: "json_col",
path: "$.user.tags",
alias: "tag",
expected: `jsonb_array_elements("json_col"->'user'->'tags') AS "tag"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got, _ := f.JSONArrayElements(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONArrayOfStrings(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "str",
expected: `jsonb_array_elements_text("data") AS "str"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "str",
expected: `jsonb_array_elements_text("data") AS "str"`,
},
{
name: "nested path",
column: "metadata",
path: "$.strings",
alias: "s",
expected: `jsonb_array_elements_text("metadata"->'strings') AS "s"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got, _ := f.JSONArrayOfStrings(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONKeys(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "k",
expected: `jsonb_each("data") AS "k"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "k",
expected: `jsonb_each("data") AS "k"`,
},
{
name: "nested path",
column: "metadata",
path: "$.object",
alias: "key",
expected: `jsonb_each("metadata"->'object') AS "key"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got, _ := f.JSONKeys(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONArrayAgg(t *testing.T) {
tests := []struct {
name string
expression string
expected string
}{
{
name: "simple column",
expression: "id",
expected: "jsonb_agg(id)",
},
{
name: "expression with function",
expression: "DISTINCT name",
expected: "jsonb_agg(DISTINCT name)",
},
{
name: "complex expression",
expression: "data->>'field'",
expected: "jsonb_agg(data->>'field')",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONArrayAgg(tt.expression))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONArrayLiteral(t *testing.T) {
tests := []struct {
name string
values []string
expected string
}{
{
name: "empty array",
values: []string{},
expected: "jsonb_build_array()",
},
{
name: "single value",
values: []string{"value1"},
expected: "jsonb_build_array('value1')",
},
{
name: "multiple values",
values: []string{"value1", "value2", "value3"},
expected: "jsonb_build_array('value1', 'value2', 'value3')",
},
{
name: "values with special characters",
values: []string{"test", "with space", "with-dash"},
expected: "jsonb_build_array('test', 'with space', 'with-dash')",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONArrayLiteral(tt.values...))
assert.Equal(t, tt.expected, got)
})
}
}
func TestConvertJSONPathToPostgresWithMode(t *testing.T) {
tests := []struct {
name string
jsonPath string
asText bool
expected string
}{
{
name: "simple path as text",
jsonPath: "$.field",
asText: true,
expected: "->>'field'",
},
{
name: "simple path as json",
jsonPath: "$.field",
asText: false,
expected: "->'field'",
},
{
name: "nested path as text",
jsonPath: "$.user.name",
asText: true,
expected: "->'user'->>'name'",
},
{
name: "nested path as json",
jsonPath: "$.user.name",
asText: false,
expected: "->'user'->'name'",
},
{
name: "deeply nested as text",
jsonPath: "$.a.b.c.d",
asText: true,
expected: "->'a'->'b'->'c'->>'d'",
},
{
name: "root path",
jsonPath: "$",
asText: true,
expected: "",
},
{
name: "empty path",
jsonPath: "",
asText: true,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New()).(*formatter)
got := f.convertJSONPathToPostgresWithMode(tt.jsonPath, tt.asText)
assert.Equal(t, tt.expected, got)
})
}
}
func TestTextToJsonColumn(t *testing.T) {
tests := []struct {
name string
column string
expected string
}{
{
name: "simple column name",
column: "data",
expected: `"data"::jsonb`,
},
{
name: "column with underscore",
column: "user_data",
expected: `"user_data"::jsonb`,
},
{
name: "column with special characters",
column: "json-col",
expected: `"json-col"::jsonb`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.TextToJsonColumn(tt.column))
assert.Equal(t, tt.expected, got)
})
}
}
func TestLowerExpression(t *testing.T) {
tests := []struct {
name string
expr string
expected string
}{
{
name: "simple column name",
expr: "name",
expected: "lower(name)",
},
{
name: "quoted column identifier",
expr: `"column_name"`,
expected: `lower("column_name")`,
},
{
name: "jsonb text extraction",
expr: "data->>'field'",
expected: "lower(data->>'field')",
},
{
name: "nested jsonb extraction",
expr: "metadata->'user'->>'name'",
expected: "lower(metadata->'user'->>'name')",
},
{
name: "jsonb_typeof expression",
expr: "jsonb_typeof(data->'field')",
expected: "lower(jsonb_typeof(data->'field'))",
},
{
name: "string concatenation",
expr: "first_name || ' ' || last_name",
expected: "lower(first_name || ' ' || last_name)",
},
{
name: "CAST expression",
expr: "CAST(value AS TEXT)",
expected: "lower(CAST(value AS TEXT))",
},
{
name: "COALESCE expression",
expr: "COALESCE(name, 'default')",
expected: "lower(COALESCE(name, 'default'))",
},
{
name: "subquery column",
expr: "users.email",
expected: "lower(users.email)",
},
{
name: "quoted identifier with special chars",
expr: `"user-name"`,
expected: `lower("user-name")`,
},
{
name: "jsonb to text cast",
expr: "data::text",
expected: "lower(data::text)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.LowerExpression(tt.expr))
assert.Equal(t, tt.expected, got)
})
}
}

View File

@@ -3,7 +3,6 @@ package postgressqlstore
import (
"context"
"database/sql"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -15,10 +14,11 @@ import (
)
type provider struct {
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
dialect *dialect
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
dialect *dialect
formatter sqlstore.SQLFormatter
}
func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] {
@@ -55,11 +55,14 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
sqldb := stdlib.OpenDBFromPool(pool)
pgDialect := pgdialect.New()
bunDB := sqlstore.NewBunDB(settings, sqldb, pgDialect, hooks)
return &provider{
settings: settings,
sqldb: sqldb,
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
dialect: new(dialect),
settings: settings,
sqldb: sqldb,
bundb: sqlstore.NewBunDB(settings, sqldb, pgDialect, hooks),
dialect: new(dialect),
formatter: newFormatter(bunDB.Dialect()),
}, nil
}
@@ -75,6 +78,10 @@ func (provider *provider) Dialect() sqlstore.SQLDialect {
return provider.dialect
}
func (provider *provider) Formatter() sqlstore.SQLFormatter {
return provider.formatter
}
func (provider *provider) BunDBCtx(ctx context.Context) bun.IDB {
return provider.bundb.BunDBCtx(ctx)
}

View File

@@ -347,6 +347,7 @@ function OnboardingAddDataSource(): JSX.Element {
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SEARCHED}`,
{
searchedDataSource: query,
resultCount: filteredDataSources.length,
},
);
}, 300);

View File

@@ -499,6 +499,9 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/alerts", am.ViewAccess(aH.AlertmanagerAPI.GetAlerts)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/keys", am.ViewAccess(aH.getRuleAttributeKeys)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/values", am.ViewAccess(aH.getRuleAttributeValues)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.ViewAccess(aH.listRules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/{id}", am.ViewAccess(aH.getRule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.EditAccess(aH.createRule)).Methods(http.MethodPost)
@@ -816,10 +819,6 @@ func (aH *APIHandler) createDowntimeSchedule(w http.ResponseWriter, r *http.Requ
return
}
if len(schedule.RuleIDs) == 0 {
schedule.SilenceAll = true
}
_, err = aH.ruleManager.MaintenanceStore().CreatePlannedMaintenance(r.Context(), schedule)
if err != nil {
render.Error(w, err)
@@ -847,10 +846,6 @@ func (aH *APIHandler) editDowntimeSchedule(w http.ResponseWriter, r *http.Reques
return
}
if len(schedule.RuleIDs) == 0 {
schedule.SilenceAll = true
}
err = aH.ruleManager.MaintenanceStore().EditPlannedMaintenance(r.Context(), schedule, id)
if err != nil {
render.Error(w, err)
@@ -1160,6 +1155,63 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter,
aH.Respond(w, res)
}
func (aH *APIHandler) getRuleAttributeKeys(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get claims from context: %v", err))
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get orgId from claims: %v", err))
return
}
searchText := r.URL.Query().Get("searchText")
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil || limit <= 0 {
limit = 10
}
keys, err := aH.ruleManager.GetSearchKeys(r.Context(), searchText, limit, orgID)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, keys)
}
func (aH *APIHandler) getRuleAttributeValues(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get claims from context: %v", err))
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get orgId from claims: %v", err))
return
}
attributeKey := r.URL.Query().Get("attributeKey")
if attributeKey == "" {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInvalidInput, "attributeKey is required"))
return
}
searchText := r.URL.Query().Get("searchText")
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil || limit <= 0 {
limit = 10
}
keys, err := aH.ruleManager.GetSearchValues(r.Context(), searchText, limit, attributeKey, orgID)
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get rule search values: %v", err))
return
}
render.Success(w, http.StatusOK, keys)
}
func (aH *APIHandler) listRules(w http.ResponseWriter, r *http.Request) {
rules, err := aH.ruleManager.ListRuleStates(r.Context())

View File

@@ -36,6 +36,16 @@ func (s AlertState) String() string {
panic(errors.Errorf("unknown alert state: %d", s))
}
func GetAllRuleStates() []string {
return []string{
StateInactive.String(),
StatePending.String(),
StateFiring.String(),
StateNoData.String(),
StateDisabled.String(),
}
}
func (s AlertState) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"log/slog"
"sort"
"strings"
@@ -1083,3 +1084,60 @@ func (m *Manager) GetAlertDetailsForMetricNames(ctx context.Context, metricNames
return result, nil
}
func (m *Manager) GetSearchKeys(ctx context.Context, searchText string, limit int, orgId valuer.UUID) ([]ruletypes.GetRuleAttributeKeys, error) {
keys, err := m.ruleStore.GetRuleLabelKeys(ctx, searchText, limit, orgId.String())
if err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "failed to get rule label keys: %v", err)
}
result := make([]ruletypes.GetRuleAttributeKeys, len(ruletypes.FixedRuleAttributeKeys))
copy(result, ruletypes.FixedRuleAttributeKeys)
for _, key := range keys {
result = append(result, ruletypes.GetRuleAttributeKeys{
Key: key,
Type: ruletypes.RuleAttributeTypeLabel,
DataType: telemetrytypes.FieldDataTypeString,
})
}
return result, nil
}
func (m *Manager) GetSearchValues(ctx context.Context, searchText string, limit int, key string, orgId valuer.UUID) ([]string, error) {
switch key {
case ruletypes.RuleAttributeKeyChannel:
return m.ruleStore.GetChannel(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyThresholdName:
return m.ruleStore.GetThresholdNames(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyCreatedBy:
return m.ruleStore.GetCreatedBy(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyUpdatedBy:
return m.ruleStore.GetUpdatedBy(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyName:
return m.ruleStore.GetNames(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyState:
allStates := model.GetAllRuleStates()
if searchText == "" {
if limit > 0 && limit < len(allStates) {
return allStates[:limit], nil
}
return allStates, nil
}
filtered := make([]string, 0)
searchLower := strings.ToLower(searchText)
for _, state := range allStates {
if strings.Contains(strings.ToLower(state), searchLower) {
filtered = append(filtered, state)
if limit > 0 && len(filtered) >= limit {
break
}
}
}
return filtered, nil
default:
return m.ruleStore.GetRuleLabelValues(ctx, searchText, limit, key, orgId.String())
}
}

View File

@@ -79,10 +79,6 @@ func (m *MockSQLRuleStore) ExpectEditRule(rule *ruletypes.Rule) {
// ExpectDeleteRule sets up SQL expectations for DeleteRule operation
func (m *MockSQLRuleStore) ExpectDeleteRule(ruleID valuer.UUID) {
plannedMaintenancePattern := `DELETE FROM "planned_maintenance_rule".+WHERE \(rule_id = '` + ruleID.StringValue() + `'\)`
m.mock.ExpectExec(plannedMaintenancePattern).
WillReturnResult(sqlmock.NewResult(0, 1))
expectedPattern := `DELETE FROM "rule".+WHERE \(id = '` + ruleID.StringValue() + `'\)`
m.mock.ExpectExec(expectedPattern).
WillReturnResult(sqlmock.NewResult(1, 1))

View File

@@ -78,7 +78,6 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
Description: maintenance.Description,
Schedule: maintenance.Schedule,
OrgID: claims.OrgID,
SilenceAll: maintenance.SilenceAll,
}
maintenanceRules := make([]*ruletypes.StorablePlannedMaintenanceRule, 0)
@@ -164,7 +163,6 @@ func (r *maintenance) EditPlannedMaintenance(ctx context.Context, maintenance ru
Description: maintenance.Description,
Schedule: maintenance.Schedule,
OrgID: claims.OrgID,
SilenceAll: maintenance.SilenceAll,
}
storablePlannedMaintenanceRules := make([]*ruletypes.StorablePlannedMaintenanceRule, 0)

View File

@@ -2,6 +2,9 @@ package sqlrulestore
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/uptrace/bun"
"strings"
"github.com/SigNoz/signoz/pkg/sqlstore"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
@@ -44,7 +47,6 @@ func (r *rule) EditRule(ctx context.Context, storedRule *ruletypes.Rule, cb func
NewUpdate().
Model(storedRule).
Where("id = ?", storedRule.ID.StringValue()).
Where("deleted = ?", false).
Exec(ctx)
if err != nil {
return err
@@ -59,20 +61,8 @@ func (r *rule) DeleteRule(ctx context.Context, id valuer.UUID, cb func(context.C
_, err := r.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(ruletypes.StorablePlannedMaintenanceRule)).
Where("rule_id = ?", id.StringValue()).
Exec(ctx)
if err != nil {
return err
}
_, err = r.sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model((*ruletypes.Rule)(nil)).
Set("deleted = ?", true).
Model(new(ruletypes.Rule)).
Where("id = ?", id.StringValue()).
Where("deleted = ?", false).
Exec(ctx)
if err != nil {
return err
@@ -92,7 +82,7 @@ func (r *rule) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.R
BunDB().
NewSelect().
Model(&rules).
Where("org_id = ?", orgID).Where("deleted = ?", false).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return rules, err
@@ -107,10 +97,212 @@ func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.Ru
BunDB().
NewSelect().
Model(rule).
Where("id = ?", id.StringValue()).Where("deleted = ?", false).
Where("id = ?", id.StringValue()).
Scan(ctx)
if err != nil {
return nil, err
}
return rule, nil
}
func (r *rule) GetRuleLabelKeys(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
labelKeys := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
fmter := r.sqlstore.Formatter()
elements, elementsAlias := fmter.JSONKeys("data", "$.labels", "keys")
elementsAliasStr := string(fmter.LowerExpression(string(elementsAlias)))
query := r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(elementsAliasStr)).
TableExpr("rule, ?", bun.SafeQuery(string(elements))).
Where("? LIKE ?", bun.SafeQuery(elementsAliasStr), searchText).
Where("org_id = ?", orgId).
Limit(limit)
err := query.Scan(ctx, &labelKeys)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "search keys for rule with orgId %s not found", orgId)
}
return labelKeys, nil
}
func (r *rule) GetThresholdNames(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
fmter := r.sqlstore.Formatter()
// Query threshold spec names
specQuery, specCol := fmter.JSONArrayElements("data", "$.condition.thresholds.spec", "spec")
nameQuery := string(fmter.JSONExtractString(string(specCol), "$.name"))
lowerNameQuery := string(fmter.LowerExpression(nameQuery))
query := r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(nameQuery)).
TableExpr("rule, ?", bun.SafeQuery(string(specQuery))).
Where("? LIKE ?", bun.SafeQuery(lowerNameQuery), searchText).
Where("org_id = ?", orgId).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "threshold names for rule with orgId %s not found", orgId)
}
if len(names) >= limit {
return names[:limit], nil
}
severityQuery := string(fmter.JSONExtractString("data", "$.labels.severity"))
lowerSeverityQuery := string(fmter.LowerExpression(severityQuery))
thresholds := make([]string, 0)
query = r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(severityQuery)).
TableExpr("rule").
Where("org_id = ?", orgId).
Where("? LIKE ?", bun.SafeQuery(lowerSeverityQuery), searchText).
Limit(limit - len(names))
err = query.Scan(ctx, &thresholds)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "threshold names for rule with orgId %s not found", orgId)
}
names = append(names, thresholds...)
return names, nil
}
func (r *rule) GetChannel(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
fmter := r.sqlstore.Formatter()
// Query v2 threshold channels
specSQL, specCol := fmter.JSONArrayElements("data", "$.condition.thresholds.spec", "spec")
channelSQL, channelCol := fmter.JSONArrayOfStrings(string(specCol), "$.channels", "channels")
lowerChannelCol := string(fmter.LowerExpression(string(channelCol)))
query := r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(string(channelCol))).
TableExpr("rule, ?, ?",
bun.SafeQuery(string(specSQL)),
bun.SafeQuery(string(channelSQL))).
Where("? LIKE ?", bun.SafeQuery(lowerChannelCol), searchText).
Where("org_id = ?", orgId).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "channel for rule with orgId %s not found", orgId)
}
if len(names) >= limit {
return names[:limit], nil
}
// Query v1 preferred channels
channelsSQL, channelsCol := fmter.JSONArrayOfStrings("data", "$.preferredChannels", "channels")
lowerChannelsCol := fmter.LowerExpression(string(channelsCol))
channels := make([]string, 0)
query = r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(string(channelsCol))).
TableExpr("rule, ?", bun.SafeQuery(string(channelsSQL))).
Where("? LIKE ?", bun.SafeQuery(string(lowerChannelsCol)), searchText).
Where("org_id = ?", orgId).
Limit(limit - len(names))
err = query.Scan(ctx, &channels)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "channel for rule with orgId %s not found", orgId)
}
names = append(names, channels...)
return names, nil
}
func (r *rule) GetNames(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
fmter := r.sqlstore.Formatter()
namePath := fmter.JSONExtractString("data", "$.alert")
lowerNamePath := fmter.LowerExpression(string(namePath))
query := r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(string(namePath))).
TableExpr("?", bun.SafeQuery("rule")).
Where("? LIKE ?", bun.SafeQuery(string(lowerNamePath)), searchText).
Where("org_id = ?", orgId).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "names for rule with orgId %s not found", orgId)
}
return names, nil
}
func (r *rule) GetCreatedBy(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
query := r.sqlstore.BunDB().NewSelect().
Distinct().
Column("created_by").
TableExpr("?", bun.SafeQuery("rule")).
Where("org_id = ?", orgId).
Where("? LIKE ?", bun.SafeQuery(string(r.sqlstore.Formatter().LowerExpression("created_by"))), searchText).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, err
}
return names, nil
}
func (r *rule) GetUpdatedBy(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
query := r.sqlstore.BunDB().NewSelect().
Distinct().
Column("updated_by").
TableExpr("?", bun.SafeQuery("rule")).
Where("org_id = ?", orgId).
Where("? LIKE ?", bun.SafeQuery(string(r.sqlstore.Formatter().LowerExpression("updated_by"))), searchText).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, err
}
return names, nil
}
func (r *rule) GetRuleLabelValues(ctx context.Context, searchText string, limit int, labelKey string, orgId string) ([]string, error) {
names := make([]string, 0)
labelPath := r.sqlstore.Formatter().JSONExtractString("data", "$.labels."+labelKey)
searchText = strings.ToLower(searchText) + "%"
query := r.sqlstore.BunDB().NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(string(labelPath))).
TableExpr("?", bun.SafeQuery("rule")).
Where("org_id = ?", orgId).
Where("? LIKE ?", bun.SafeQuery(string(r.sqlstore.Formatter().LowerExpression(string(labelPath)))), searchText).Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "search values for rule with orgId %s not found", orgId)
}
return names, nil
}

View File

@@ -138,7 +138,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewUpdateTTLSettingForCustomRetentionFactory(sqlstore, sqlschema),
sqlmigration.NewAddRoutePolicyFactory(sqlstore, sqlschema),
sqlmigration.NewAddAuthTokenFactory(sqlstore, sqlschema),
sqlmigration.NewAddSilenceAllColumnFactory(sqlstore, sqlschema),
)
}

View File

@@ -1,155 +0,0 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"log/slog"
)
type addSilenceAllColumn struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
logger *slog.Logger
}
type plannedMaintenance struct {
bun.BaseModel `bun:"table:planned_maintenance"`
types.Identifiable
SilenceAll bool `bun:"silence_all,type:boolean"`
}
type plannedMaintenanceRule 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"`
}
func NewAddSilenceAllColumnFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_silence_all_column"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return newAddSilenceAllColumn(ctx, providerSettings, config, sqlstore, sqlschema)
})
}
func newAddSilenceAllColumn(_ context.Context, settings factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
return &addSilenceAllColumn{
sqlstore: sqlstore,
sqlschema: sqlschema,
logger: settings.Logger,
}, nil
}
func (migration *addSilenceAllColumn) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addSilenceAllColumn) Up(ctx context.Context, db *bun.DB) error {
table, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("planned_maintenance"))
if err != nil {
return err
}
for _, column := range table.Columns {
if column.Name == "silence_all" {
return nil
}
}
var joinTableBackup []plannedMaintenanceRule
err = db.NewSelect().
Model(&joinTableBackup).
Scan(ctx)
if err != nil {
return errors.NewInternalf(errors.CodeInternal, "failed to backup planned_maintenance_rule data: %v", err)
}
maintenanceIDsMap := make(map[string]bool)
for _, record := range joinTableBackup {
maintenanceIDsMap[record.PlannedMaintenanceID.StringValue()] = true
}
var maintenanceIDsWithRules []string
for id := range maintenanceIDsMap {
maintenanceIDsWithRules = append(maintenanceIDsWithRules, id)
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
sqls := [][]byte{}
column := &sqlschema.Column{
Name: "silence_all",
DataType: sqlschema.DataTypeBoolean,
Nullable: false,
Default: "false",
}
columnSQLs := migration.sqlschema.Operator().AddColumn(table, nil, column, nil)
sqls = append(sqls, columnSQLs...)
for _, sqlStmt := range sqls {
if _, err := tx.ExecContext(ctx, string(sqlStmt)); err != nil {
return err
}
}
if len(joinTableBackup) > 0 {
_, err = tx.NewInsert().
Model(&joinTableBackup).
Exec(ctx)
if err != nil {
return errors.NewInternalf(errors.CodeInternal, "failed to restore planned_maintenance_rule data: %v", err)
}
}
err = migration.backfillSilenceAll(ctx, tx, maintenanceIDsWithRules)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *addSilenceAllColumn) backfillSilenceAll(ctx context.Context, tx bun.Tx, maintenanceIDsWithRules []string) error {
if len(maintenanceIDsWithRules) == 0 {
_, err := tx.NewUpdate().
Model((*plannedMaintenance)(nil)).
Set("silence_all = ?", true).
Where("1 = 1").
Exec(ctx)
return err
}
_, err := tx.NewUpdate().
Model((*plannedMaintenance)(nil)).
Set("silence_all = ?", true).
Where("id NOT IN (?)", bun.In(maintenanceIDsWithRules)).
Exec(ctx)
return err
}
func (migration *addSilenceAllColumn) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -0,0 +1,113 @@
package sqlitesqlstore
import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun/schema"
)
type formatter struct {
bunf schema.Formatter
}
func newFormatter(dialect schema.Dialect) sqlstore.SQLFormatter {
return &formatter{bunf: schema.NewFormatter(dialect)}
}
func (f *formatter) JSONExtractString(column, path string) []byte {
var sql []byte
sql = append(sql, "json_extract("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "')"...)
return sql
}
func (f *formatter) JSONType(column, path string) []byte {
var sql []byte
sql = append(sql, "json_type("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "')"...)
return sql
}
func (f *formatter) JSONIsArray(column, path string) []byte {
var sql []byte
sql = append(sql, f.JSONType(column, path)...)
sql = append(sql, " = 'array'"...)
return sql
}
func (f *formatter) JSONArrayElements(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "json_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "'"...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".value")
}
func (f *formatter) JSONArrayOfStrings(column, path, alias string) ([]byte, []byte) {
return f.JSONArrayElements(column, path, alias)
}
func (f *formatter) JSONKeys(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "json_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "'"...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".key")
}
func (f *formatter) JSONArrayAgg(expression string) []byte {
var sql []byte
sql = append(sql, "json_group_array("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}
func (f *formatter) JSONArrayLiteral(values ...string) []byte {
if len(values) == 0 {
return []byte("json_array()")
}
var sql []byte
sql = append(sql, "json_array("...)
for i, v := range values {
if i > 0 {
sql = append(sql, ", "...)
}
sql = append(sql, '\'')
sql = append(sql, v...)
sql = append(sql, '\'')
}
sql = append(sql, ')')
return sql
}
func (f *formatter) TextToJsonColumn(column string) []byte {
return f.bunf.AppendIdent([]byte{}, column)
}
func (f *formatter) LowerExpression(expression string) []byte {
var sql []byte
sql = append(sql, "lower("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}

View File

@@ -0,0 +1,397 @@
package sqlitesqlstore
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/uptrace/bun/dialect/sqlitedialect"
)
func TestJSONExtractString(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.field",
expected: `json_extract("data", '$.field')`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.name",
expected: `json_extract("metadata", '$.user.name')`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `json_extract("json_col", '$')`,
},
{
name: "array index path",
column: "items",
path: "$.list[0]",
expected: `json_extract("items", '$.list[0]')`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONExtractString(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONType(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.field",
expected: `json_type("data", '$.field')`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.age",
expected: `json_type("metadata", '$.user.age')`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `json_type("json_col", '$')`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONType(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONIsArray(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.items",
expected: `json_type("data", '$.items') = 'array'`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.tags",
expected: `json_type("metadata", '$.user.tags') = 'array'`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `json_type("json_col", '$') = 'array'`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONIsArray(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONArrayElements(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "elem",
expected: `json_each("data") AS "elem"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "elem",
expected: `json_each("data") AS "elem"`,
},
{
name: "nested path",
column: "metadata",
path: "$.items",
alias: "item",
expected: `json_each("metadata", '$.items') AS "item"`,
},
{
name: "deeply nested path",
column: "json_col",
path: "$.user.tags",
alias: "tag",
expected: `json_each("json_col", '$.user.tags') AS "tag"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got, _ := f.JSONArrayElements(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONArrayOfStrings(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "str",
expected: `json_each("data") AS "str"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "str",
expected: `json_each("data") AS "str"`,
},
{
name: "nested path",
column: "metadata",
path: "$.strings",
alias: "s",
expected: `json_each("metadata", '$.strings') AS "s"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got, _ := f.JSONArrayOfStrings(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONKeys(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "k",
expected: `json_each("data") AS "k"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "k",
expected: `json_each("data") AS "k"`,
},
{
name: "nested path",
column: "metadata",
path: "$.object",
alias: "key",
expected: `json_each("metadata", '$.object') AS "key"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got, _ := f.JSONKeys(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONArrayAgg(t *testing.T) {
tests := []struct {
name string
expression string
expected string
}{
{
name: "simple column",
expression: "id",
expected: "json_group_array(id)",
},
{
name: "expression with function",
expression: "DISTINCT name",
expected: "json_group_array(DISTINCT name)",
},
{
name: "complex expression",
expression: "json_extract(data, '$.field')",
expected: "json_group_array(json_extract(data, '$.field'))",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONArrayAgg(tt.expression))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONArrayLiteral(t *testing.T) {
tests := []struct {
name string
values []string
expected string
}{
{
name: "empty array",
values: []string{},
expected: "json_array()",
},
{
name: "single value",
values: []string{"value1"},
expected: "json_array('value1')",
},
{
name: "multiple values",
values: []string{"value1", "value2", "value3"},
expected: "json_array('value1', 'value2', 'value3')",
},
{
name: "values with special characters",
values: []string{"test", "with space", "with-dash"},
expected: "json_array('test', 'with space', 'with-dash')",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONArrayLiteral(tt.values...))
assert.Equal(t, tt.expected, got)
})
}
}
func TestTextToJsonColumn(t *testing.T) {
tests := []struct {
name string
column string
expected string
}{
{
name: "simple column name",
column: "data",
expected: `"data"`,
},
{
name: "column with underscore",
column: "user_data",
expected: `"user_data"`,
},
{
name: "column with special characters",
column: "json-col",
expected: `"json-col"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.TextToJsonColumn(tt.column))
assert.Equal(t, tt.expected, got)
})
}
}
func TestLowerExpression(t *testing.T) {
tests := []struct {
name string
expr string
expected string
}{
{
name: "json_extract expression",
expr: "json_extract(data, '$.field')",
expected: "lower(json_extract(data, '$.field'))",
},
{
name: "nested json_extract",
expr: "json_extract(metadata, '$.user.name')",
expected: "lower(json_extract(metadata, '$.user.name'))",
},
{
name: "json_type expression",
expr: "json_type(data, '$.field')",
expected: "lower(json_type(data, '$.field'))",
},
{
name: "string concatenation",
expr: "first_name || ' ' || last_name",
expected: "lower(first_name || ' ' || last_name)",
},
{
name: "CAST expression",
expr: "CAST(value AS TEXT)",
expected: "lower(CAST(value AS TEXT))",
},
{
name: "COALESCE expression",
expr: "COALESCE(name, 'default')",
expected: "lower(COALESCE(name, 'default'))",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.LowerExpression(tt.expr))
assert.Equal(t, tt.expected, got)
})
}
}

View File

@@ -17,10 +17,11 @@ import (
)
type provider struct {
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
dialect *dialect
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
dialect *dialect
formatter sqlstore.SQLFormatter
}
func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] {
@@ -54,11 +55,14 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
settings.Logger().InfoContext(ctx, "connected to sqlite", "path", config.Sqlite.Path)
sqldb.SetMaxOpenConns(config.Connection.MaxOpenConns)
sqliteDialect := sqlitedialect.New()
bunDB := sqlstore.NewBunDB(settings, sqldb, sqliteDialect, hooks)
return &provider{
settings: settings,
sqldb: sqldb,
bundb: sqlstore.NewBunDB(settings, sqldb, sqlitedialect.New(), hooks),
dialect: new(dialect),
settings: settings,
sqldb: sqldb,
bundb: bunDB,
dialect: new(dialect),
formatter: newFormatter(bunDB.Dialect()),
}, nil
}
@@ -74,6 +78,10 @@ func (provider *provider) Dialect() sqlstore.SQLDialect {
return provider.dialect
}
func (provider *provider) Formatter() sqlstore.SQLFormatter {
return provider.formatter
}
func (provider *provider) BunDBCtx(ctx context.Context) bun.IDB {
return provider.bundb.BunDBCtx(ctx)
}

View File

@@ -20,6 +20,8 @@ type SQLStore interface {
// Returns the dialect of the database.
Dialect() SQLDialect
Formatter() SQLFormatter
// RunInTxCtx runs the given callback in a transaction. It creates and injects a new context with the transaction.
// If a transaction is present in the context, it will be used.
RunInTxCtx(ctx context.Context, opts *SQLStoreTxOptions, cb func(ctx context.Context) error) error
@@ -86,3 +88,35 @@ type SQLDialect interface {
// as an argument.
ToggleForeignKeyConstraint(ctx context.Context, bun *bun.DB, enable bool) error
}
type SQLFormatter interface {
// JSONExtractString takes path in sqlite format like "$.labels.severity"
JSONExtractString(column, path string) []byte
// JSONType used to determine the type of the value extracted from the path
JSONType(column, path string) []byte
// JSONIsArray used to check whether the value is array or not
JSONIsArray(column, path string) []byte
// JSONArrayElements returns query as well as columns alias to be used for select and where clause
JSONArrayElements(column, path, alias string) ([]byte, []byte)
// JSONArrayOfStrings returns query as well as columns alias to be used for select and where clause
JSONArrayOfStrings(column, path, alias string) ([]byte, []byte)
// JSONArrayAgg aggregates values into a JSON array
JSONArrayAgg(expression string) []byte
// JSONArrayLiteral creates a literal JSON array from the given string values
JSONArrayLiteral(values ...string) []byte
// JSONKeys return extracted key from json as well as alias to be used for select and where clause
JSONKeys(column, path, alias string) ([]byte, []byte)
// TextToJsonColumn converts a text column to JSON type
TextToJsonColumn(column string) []byte
// LowerExpression wraps any SQL expression with lower() function for case-insensitive operations
LowerExpression(expression string) []byte
}

View File

@@ -0,0 +1,112 @@
package sqlstoretest
import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun/schema"
)
type formatter struct {
bunf schema.Formatter
}
func newFormatter(dialect schema.Dialect) sqlstore.SQLFormatter {
return &formatter{bunf: schema.NewFormatter(dialect)}
}
func (f *formatter) JSONExtractString(column, path string) []byte {
var sql []byte
sql = append(sql, "json_extract("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "')"...)
return sql
}
func (f *formatter) JSONType(column, path string) []byte {
var sql []byte
sql = append(sql, "json_type("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "')"...)
return sql
}
func (f *formatter) JSONIsArray(column, path string) []byte {
var sql []byte
sql = append(sql, f.JSONType(column, path)...)
sql = append(sql, " = 'array'"...)
return sql
}
func (f *formatter) JSONArrayElements(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "json_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "'"...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".value")
}
func (f *formatter) JSONArrayOfStrings(column, path, alias string) ([]byte, []byte) {
return f.JSONArrayElements(column, path, alias)
}
func (f *formatter) JSONKeys(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "json_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "'"...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".key")
}
func (f *formatter) JSONArrayAgg(expression string) []byte {
var sql []byte
sql = append(sql, "json_group_array("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}
func (f *formatter) JSONArrayLiteral(values ...string) []byte {
if len(values) == 0 {
return []byte("json_array()")
}
var sql []byte
sql = append(sql, "json_array("...)
for i, v := range values {
if i > 0 {
sql = append(sql, ", "...)
}
sql = append(sql, '\'')
sql = append(sql, v...)
sql = append(sql, '\'')
}
sql = append(sql, ')')
return sql
}
func (f *formatter) TextToJsonColumn(column string) []byte {
return f.bunf.AppendIdent([]byte{}, column)
}
func (f *formatter) LowerExpression(expression string) []byte {
var sql []byte
sql = append(sql, "lower("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}

View File

@@ -15,10 +15,11 @@ import (
var _ sqlstore.SQLStore = (*Provider)(nil)
type Provider struct {
db *sql.DB
mock sqlmock.Sqlmock
bunDB *bun.DB
dialect *dialect
db *sql.DB
mock sqlmock.Sqlmock
bunDB *bun.DB
dialect *dialect
formatter sqlstore.SQLFormatter
}
func New(config sqlstore.Config, matcher sqlmock.QueryMatcher) *Provider {
@@ -38,10 +39,11 @@ func New(config sqlstore.Config, matcher sqlmock.QueryMatcher) *Provider {
}
return &Provider{
db: db,
mock: mock,
bunDB: bunDB,
dialect: new(dialect),
db: db,
mock: mock,
bunDB: bunDB,
dialect: new(dialect),
formatter: newFormatter(bunDB.Dialect()),
}
}
@@ -61,6 +63,8 @@ func (provider *Provider) Dialect() sqlstore.SQLDialect {
return provider.dialect
}
func (provider *Provider) Formatter() sqlstore.SQLFormatter { return provider.formatter }
func (provider *Provider) BunDBCtx(ctx context.Context) bun.IDB {
return provider.bunDB
}

View File

@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"slices"
"time"
"unicode/utf8"
@@ -452,3 +454,18 @@ func (g *GettableRule) MarshalJSON() ([]byte, error) {
return json.Marshal(aux)
}
}
type RuleAttributeKeyType struct {
valuer.String
}
var (
RuleAttributeTypeFixed = RuleAttributeKeyType{valuer.NewString("fixed")}
RuleAttributeTypeLabel = RuleAttributeKeyType{valuer.NewString("label")}
)
type GetRuleAttributeKeys struct {
Key string `json:"key"`
DataType telemetrytypes.FieldDataType `json:"dataType"`
Type RuleAttributeKeyType `json:"type"`
}

View File

@@ -1,5 +1,31 @@
package ruletypes
const CriticalThresholdName = "CRITICAL"
const LabelThresholdName = "threshold.name"
const LabelRuleId = "ruleId"
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
const (
CriticalThresholdName = "CRITICAL"
LabelThresholdName = "threshold.name"
LabelRuleId = "ruleId"
// Rule attribute key constants for search and filtering
RuleAttributeKeyCreatedBy = "created_by"
RuleAttributeKeyUpdatedBy = "updated_by"
RuleAttributeKeyName = "name"
RuleAttributeKeyThresholdName = "threshold.name"
RuleAttributeKeyPolicy = "policy"
RuleAttributeKeyChannel = "channel"
RuleAttributeKeyState = "state"
//RuleAttributeKeyRuleType = "type"
)
var (
FixedRuleAttributeKeys = []GetRuleAttributeKeys{
{Key: RuleAttributeKeyCreatedBy, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyUpdatedBy, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyName, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyThresholdName, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyChannel, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyPolicy, DataType: telemetrytypes.FieldDataTypeBool, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyState, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
}
)

View File

@@ -24,7 +24,6 @@ type StorablePlannedMaintenance struct {
Description string `bun:"description,type:text"`
Schedule *Schedule `bun:"schedule,type:text,notnull"`
OrgID string `bun:"org_id,type:text"`
SilenceAll bool `bun:"silence_all,type:boolean"`
}
type GettablePlannedMaintenance struct {
@@ -39,7 +38,6 @@ type GettablePlannedMaintenance struct {
UpdatedBy string `json:"updatedBy"`
Status string `json:"status"`
Kind string `json:"kind"`
SilenceAll bool `json:"silenceAll"`
}
type StorablePlannedMaintenanceRule struct {
@@ -66,7 +64,7 @@ func (m *GettablePlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bo
}
}
// If no alert ids, then skip all alerts
if m.SilenceAll {
if len(m.RuleIDs) == 0 {
found = true
}
@@ -297,7 +295,6 @@ func (m GettablePlannedMaintenance) MarshalJSON() ([]byte, error) {
UpdatedBy string `json:"updatedBy" db:"updated_by"`
Status string `json:"status"`
Kind string `json:"kind"`
SilenceAll bool `json:"silenceAll" db:"silence_all"`
}{
Id: m.Id,
Name: m.Name,
@@ -310,7 +307,6 @@ func (m GettablePlannedMaintenance) MarshalJSON() ([]byte, error) {
UpdatedBy: m.UpdatedBy,
Status: status,
Kind: kind,
SilenceAll: m.SilenceAll,
})
}
@@ -332,7 +328,6 @@ func (m *GettablePlannedMaintenanceRule) ConvertGettableMaintenanceRuleToGettabl
UpdatedAt: m.UpdatedAt,
CreatedBy: m.CreatedBy,
UpdatedBy: m.UpdatedBy,
SilenceAll: m.SilenceAll,
}
}

View File

@@ -21,7 +21,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "only-on-saturday",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "Europe/London",
Recurrence: &Recurrence{
@@ -39,7 +38,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "weekly-across-midnight-previous-day",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -57,7 +55,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "weekly-across-midnight-previous-day",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -75,7 +72,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "weekly-across-midnight-previous-day",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -93,7 +89,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "weekly-across-midnight-previous-day-not-in-repeaton",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -111,7 +106,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "daily-maintenance-across-midnight",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -128,7 +122,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "at-start-time-boundary",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -145,7 +138,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "at-end-time-boundary",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -162,7 +154,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "monthly-multi-day-duration",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -179,7 +170,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "weekly-multi-day-duration",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -197,7 +187,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "monthly-crosses-to-next-month",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -214,7 +203,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "timezone-offset-test",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "America/New_York", // UTC-5 or UTC-4 depending on DST
Recurrence: &Recurrence{
@@ -231,7 +219,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "daily-maintenance-time-outside-window",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -248,7 +235,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring-maintenance-with-past-end-date",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -266,7 +252,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "monthly-maintenance-spans-month-end",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -283,7 +268,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "weekly-empty-repeaton",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -301,7 +285,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "monthly-maintenance-february-fewer-days",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -317,7 +300,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "daily-maintenance-crosses-midnight",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -333,7 +315,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "monthly-maintenance-crosses-month-end",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -349,7 +330,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -365,7 +345,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "weekly-maintenance-crosses-midnight",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -382,7 +361,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -398,7 +376,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "daily-maintenance-crosses-midnight",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -414,7 +391,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-hours",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -430,7 +406,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "fixed planned maintenance start <= ts <= end",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Now().UTC().Add(-time.Hour),
@@ -443,7 +418,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "fixed planned maintenance start >= ts",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Now().UTC().Add(time.Hour),
@@ -456,7 +430,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "fixed planned maintenance ts < start",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Now().UTC().Add(time.Hour),
@@ -469,7 +442,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat sunday, saturday, weekly for 24 hours, in Us/Eastern timezone",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "US/Eastern",
Recurrence: &Recurrence{
@@ -486,7 +458,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -502,7 +473,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -518,7 +488,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -534,7 +503,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -551,7 +519,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -568,7 +535,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -585,7 +551,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -602,7 +567,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -619,7 +583,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -635,7 +598,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -651,7 +613,6 @@ func TestShouldSkipMaintenance(t *testing.T) {
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &GettablePlannedMaintenance{
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
@@ -673,144 +634,3 @@ func TestShouldSkipMaintenance(t *testing.T) {
}
}
}
func TestSilenceAllFunctionality(t *testing.T) {
cases := []struct {
name string
maintenance *GettablePlannedMaintenance
ruleID string
ts time.Time
skip bool
}{
{
name: "SilenceAll=true with specific ruleIDs - should silence rule in list during maintenance window",
maintenance: &GettablePlannedMaintenance{
RuleIDs: []string{"rule-1", "rule-2"},
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: Duration(time.Hour * 2),
RepeatType: RepeatTypeDaily,
},
},
},
ruleID: "rule-1",
ts: time.Date(2024, 1, 1, 12, 30, 0, 0, time.UTC),
skip: true,
},
{
name: "SilenceAll=true with specific ruleIDs - should silence rule NOT in list during maintenance window",
maintenance: &GettablePlannedMaintenance{
RuleIDs: []string{"rule-1", "rule-2"},
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: Duration(time.Hour * 2),
RepeatType: RepeatTypeDaily,
},
},
},
ruleID: "rule-3",
ts: time.Date(2024, 1, 1, 12, 30, 0, 0, time.UTC),
skip: true,
},
{
name: "SilenceAll=false with specific ruleIDs - should silence rule in list during maintenance window",
maintenance: &GettablePlannedMaintenance{
RuleIDs: []string{"rule-1", "rule-2"},
SilenceAll: false,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: Duration(time.Hour * 2),
RepeatType: RepeatTypeDaily,
},
},
},
ruleID: "rule-1",
ts: time.Date(2024, 1, 1, 12, 30, 0, 0, time.UTC),
skip: true,
},
{
name: "SilenceAll=true with empty ruleIDs - should silence all rules during maintenance window",
maintenance: &GettablePlannedMaintenance{
RuleIDs: []string{},
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: Duration(time.Hour * 2),
RepeatType: RepeatTypeDaily,
},
},
},
ruleID: "any-rule",
ts: time.Date(2024, 1, 1, 12, 30, 0, 0, time.UTC),
skip: true,
},
{
name: "SilenceAll=false with empty ruleIDs - should not silence any rules",
maintenance: &GettablePlannedMaintenance{
RuleIDs: []string{},
SilenceAll: false,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: Duration(time.Hour * 2),
RepeatType: RepeatTypeDaily,
},
},
},
ruleID: "any-rule",
ts: time.Date(2024, 1, 1, 12, 30, 0, 0, time.UTC),
skip: false,
},
{
name: "SilenceAll=true with fixed maintenance window - should not skip outside window",
maintenance: &GettablePlannedMaintenance{
RuleIDs: []string{},
SilenceAll: true,
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
EndTime: time.Date(2024, 1, 1, 14, 0, 0, 0, time.UTC),
},
},
ruleID: "any-rule",
ts: time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC),
skip: false,
},
{
name: "SilenceAll=false with nil ruleIDs - should not silence any rules",
maintenance: &GettablePlannedMaintenance{
RuleIDs: nil,
SilenceAll: false,
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: Duration(time.Hour * 2),
RepeatType: RepeatTypeDaily,
},
},
},
ruleID: "any-rule",
ts: time.Date(2024, 1, 1, 12, 30, 0, 0, time.UTC),
skip: false,
},
}
for idx, c := range cases {
result := c.maintenance.ShouldSkip(c.ruleID, c.ts)
if result != c.skip {
t.Errorf("skip %v, got %v, case:%d - %s", c.skip, result, idx, c.name)
}
}
}

View File

@@ -53,4 +53,11 @@ type RuleStore interface {
DeleteRule(context.Context, valuer.UUID, func(context.Context) error) error
GetStoredRules(context.Context, string) ([]*Rule, error)
GetStoredRule(context.Context, valuer.UUID) (*Rule, error)
GetRuleLabelKeys(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetThresholdNames(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetChannel(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetNames(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetCreatedBy(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetUpdatedBy(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetRuleLabelValues(ctx context.Context, searchText string, limit int, labelKey string, orgId string) ([]string, error)
}