Compare commits

...

73 Commits

Author SHA1 Message Date
nikhilmantri0902
75f2fdc4c0 chore: sql mock expectation added 2025-12-16 13:19:12 +05:30
nikhilmantri0902
3f5592929d fix: remove unnecessary yarn file 2025-12-16 12:45:35 +05:30
nikhilmantri0902
814bc3aaa2 chore: merged main and resolved conflicts 2025-12-16 12:26:23 +05:30
Srikanth Chekuri
a6cfcde178 Merge branch 'main' into feat/metric_name_alerts_api 2025-12-15 18:29:03 +05:30
nikhilmantri0902
101ddb36cd chore: moved query parser to rulestore and filter rules for metric names 2025-12-15 18:16:44 +05:30
nikhilmantri0902
93afdac648 chore: merged main and resolved conflicts 2025-12-15 15:28:54 +05:30
nikhilmantri0902
9e4b9a6037 chore: parser API added 2025-12-03 20:33:41 +05:30
nikhilmantri0902
a7837e142c chore: merged main and added queryparser 2025-12-03 20:12:05 +05:30
nikhilmantri0902
724919a6e8 chore: orgID 2025-12-01 21:58:39 +05:30
nikhilmantri0902
d81909e911 chore: getMetricAlerts spec modified to use postable struct 2025-12-01 15:50:23 +05:30
nikhilmantri0902
a60f89f263 chore: getMetricAlerts spec modified to use postable struct 2025-12-01 15:46:47 +05:30
nikhilmantri0902
4506e9ae65 fix: remove fmt error 2025-11-30 15:28:13 +05:30
nikhilmantri0902
0b130843a2 chore: added types 2025-11-30 15:22:47 +05:30
nikhilmantri0902
f6be2bfac2 chore: removed DI from signoz struct 2025-11-26 18:11:23 +05:30
nikhilmantri0902
4daf8ea48c chore: moved alerts to v2 module 2025-11-26 16:20:57 +05:30
nikhilmantri0902
5cd566c5f2 Merge branch 'feat/metrics_explorer_module' into feat/metric_name_alerts_api 2025-11-26 15:02:14 +05:30
nikhilmantri0902
57a669bec8 fix: test cases 2025-11-25 20:51:03 +05:30
Nikhil Mantri
b3acc2301f Merge branch 'main' into feat/metrics_explorer_module 2025-11-25 20:42:27 +05:30
nikhilmantri0902
e872d3a2d4 chore: modify telemetrymetrics tableToUse funcs, use them 2025-11-25 20:36:20 +05:30
nikhilmantri0902
86cac2f23b chore: telemetrymetadatastore is now a part of the signoz struct 2025-11-25 16:52:18 +05:30
nikhilmantri0902
57b04d1d72 chore: small PR review related changes 2025-11-25 16:41:48 +05:30
nikhilmantri0902
eed95d943a chore: moved to metricmoduletypes 2025-11-25 14:44:26 +05:30
nikhilmantri0902
2f95ab80e7 chore: api added GetMetricMetadata, broken down GetMetricsMetadataMulti 2025-11-25 14:12:31 +05:30
nikhilmantri0902
93f46b2509 chore: samples treemap refactor to a single query 2025-11-25 12:38:06 +05:30
Srikanth Chekuri
f39c4bd27f Merge branch 'main' into feat/metrics_explorer_module 2025-11-25 00:44:17 +05:30
nikhilmantri0902
23a14fa8ca chore: using errors package for errors in metrictypes 2025-11-24 20:48:13 +05:30
nikhilmantri0902
e1474e65c6 chore: defined scan and value method for type and temporality 2025-11-24 19:28:48 +05:30
nikhilmantri0902
3f23e9c6c5 chore: filter where clause 2025-11-24 18:43:32 +05:30
nikhilmantri0902
7cac4477d2 chore: removed unused returns from buildFilter expression 2025-11-24 18:42:14 +05:30
nikhilmantri0902
69da708960 chore: moved queries to sqlbuilder 2025-11-24 17:54:08 +05:30
nikhilmantri0902
1f33bebc44 chore: moved GetMetricsMetadataMulti and UpdateMetricsMetadata queries to sqlBuilder 2025-11-24 16:08:37 +05:30
nikhilmantri0902
f5a06f2ef5 fix: fetch temporality from timeseriestable in GetMetricsMetadataMulti 2025-11-24 15:38:12 +05:30
nikhilmantri0902
cde07aeb03 chore: added validation helpers for request structs 2025-11-24 15:24:29 +05:30
nikhilmantri0902
8a1f77b0a8 fix: lint 2025-11-24 15:02:04 +05:30
nikhilmantri0902
88c0e489e8 chore: rename resolveOrderBy -> getStatsOrderByColumn 2025-11-24 14:05:36 +05:30
nikhilmantri0902
973ede64b9 chore: removed helpers_test 2025-11-24 14:04:22 +05:30
nikhilmantri0902
e567fed291 chore: modified GetStats to do fetching and sorting via query 2025-11-24 13:55:50 +05:30
nikhilmantri0902
6eaff5bcac chore: using qbtypes filter 2025-11-24 00:24:47 +05:30
Nikhil Mantri
5bbf1a9f5f Merge branch 'main' into feat/metrics_explorer_module 2025-11-24 00:23:05 +05:30
nikhilmantri0902
5afda70ebe chore: added cache dependency back to metricsmodule 2025-11-23 23:53:48 +05:30
nikhilmantri0902
dcd2dea91c chore: rearrange 2025-11-23 23:28:23 +05:30
nikhilmantri0902
2ab0e1e378 chore: added mapping and reverse mapping functions from DB layer 2025-11-23 23:16:09 +05:30
nikhilmantri0902
4d42a8368a fix: added is_monotonic property fetching from updated_metadata 2025-11-23 22:19:29 +05:30
nikhilmantri0902
c35fa1bfb0 chore: removed table constants 2025-11-23 19:17:03 +05:30
nikhilmantri0902
3d3648a5a8 chore: returning nil in case of error from GetUpdatedMetricsMetadata 2025-11-23 19:10:44 +05:30
nikhilmantri0902
09786dd626 fix: __normalized is now false by default 2025-11-23 19:01:41 +05:30
nikhilmantri0902
b369b8495f chore: now injecting telemetrymetadatastore as a dependency initialized in main signoz repo 2025-11-23 18:58:35 +05:30
nikhilmantri0902
846f8901fc chore: moved treemap type to metrictypes 2025-11-23 18:41:49 +05:30
nikhilmantri0902
3343c023af fix: snake case json tags to camel case and treemap using valuer.string 2025-11-23 18:33:45 +05:30
nikhilmantri0902
5129fa65b9 chore: added TODO 2025-11-23 16:12:22 +05:30
nikhilmantri0902
dddf485c4f chore: db update 2025-11-23 16:11:02 +05:30
nikhilmantri0902
6ccfbdbedf chore: added v2 update metric_metadata API 2025-11-23 15:44:26 +05:30
nikhilmantri0902
8eb7be801a chore: update orderBy -> qbvytypes.Orderby 2025-11-23 01:32:10 +05:30
nikhilmantri0902
09eca10f07 chore: added caching as a dependency of metricsmodule 2025-11-21 16:30:05 +05:30
nikhilmantri0902
8f6976570b chore: temporality fetching from metadata 2025-11-21 12:46:21 +05:30
nikhilmantri0902
d9a81d8141 chore: seperate API for alerts 2025-11-20 19:07:19 +05:30
nikhilmantri0902
5944aaf1fb fix: same metric on multiple pages fix using secondary order by metric_name 2025-11-20 11:58:59 +05:30
Nikhil Mantri
3527603525 Merge branch 'main' into feat/metrics_explorer_module 2025-11-20 10:59:21 +05:30
Nikhil Mantri
d44f2e0b6d Merge branch 'main' into feat/metrics_explorer_module 2025-11-17 22:49:56 +05:30
Nikhil Mantri
6456ed1c9d Merge branch 'main' into feat/metrics_explorer_module 2025-11-16 16:26:53 +05:30
nikhilmantri0902
658bcc92aa chore: cleanup test 2025-11-16 16:26:03 +05:30
nikhilmantri0902
16aa13238a chore: added unit tests 2025-11-16 14:20:49 +05:30
nikhilmantri0902
ec0764fbb0 chore: some helper comments added 2025-11-16 13:58:27 +05:30
nikhilmantri0902
99cdb03849 chore: reorganization of code 2025-11-16 13:46:13 +05:30
nikhilmantri0902
0c3fdeebc0 chore: using slices contains 2025-11-16 12:09:56 +05:30
nikhilmantri0902
e70aaea5ce chore: signature improvement 2025-11-16 11:58:52 +05:30
nikhilmantri0902
7c234cc278 chore: cleanup and constants file new 2025-11-15 22:43:50 +05:30
nikhilmantri0902
0e62d5e410 chore: added a TODO on a design definition 2025-11-15 22:12:16 +05:30
nikhilmantri0902
2eb698f669 chore: where clause improvement 2025-11-15 21:59:42 +05:30
nikhilmantri0902
bd47ff0d0d chore: added treemap building part 2025-11-14 02:15:12 +05:30
nikhilmantri0902
c9d6e7bd98 chore: get stats functions 2025-11-14 01:45:40 +05:30
nikhilmantri0902
e28b7622f8 chore: order -> orderBy 2025-11-13 22:03:03 +05:30
nikhilmantri0902
ec87ed9a22 chore: setup skeleton 2025-11-13 21:55:15 +05:30
12 changed files with 216 additions and 20 deletions

View File

@@ -3,13 +3,14 @@ package app
import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
_ "net/http/pprof" // http profiler
"slices"
"github.com/SigNoz/signoz/pkg/cache/memorycache"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
"go.opentelemetry.io/otel/propagation"
@@ -106,7 +107,8 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
signoz.Prometheus,
signoz.Modules.OrgGetter,
signoz.Querier,
signoz.Instrumentation.Logger(),
signoz.Instrumentation.ToProviderSettings(),
signoz.QueryParser,
)
if err != nil {
@@ -353,8 +355,8 @@ func (s *Server) Stop(ctx context.Context) error {
return nil
}
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, logger *slog.Logger) (*baserules.Manager, error) {
ruleStore := sqlrulestore.NewRuleStore(sqlstore)
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
// create manager opts
managerOpts := &baserules.ManagerOptions{
@@ -364,7 +366,7 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
Logger: zap.L(),
Reader: ch,
Querier: querier,
SLogger: logger,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,

View File

@@ -137,6 +137,28 @@ func (h *handler) GetMetricMetadata(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, metadata)
}
func (h *handler) GetMetricAlerts(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
metricName := strings.TrimSpace(req.URL.Query().Get("metricName"))
if metricName == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName query parameter is required"))
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
out, err := h.module.GetMetricAlerts(req.Context(), orgID, metricName)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, out)
}
func (h *handler) GetMetricDashboards(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {

View File

@@ -20,6 +20,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/metricsexplorertypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
sqlbuilder "github.com/huandu/go-sqlbuilder"
@@ -33,12 +34,13 @@ type module struct {
condBuilder qbtypes.ConditionBuilder
logger *slog.Logger
cache cache.Cache
ruleStore ruletypes.RuleStore
dashboardModule dashboard.Module
config metricsexplorer.Config
}
// NewModule constructs the metrics module with the provided dependencies.
func NewModule(ts telemetrystore.TelemetryStore, telemetryMetadataStore telemetrytypes.MetadataStore, cache cache.Cache, dashboardModule dashboard.Module, providerSettings factory.ProviderSettings, cfg metricsexplorer.Config) metricsexplorer.Module {
func NewModule(ts telemetrystore.TelemetryStore, telemetryMetadataStore telemetrytypes.MetadataStore, cache cache.Cache, ruleStore ruletypes.RuleStore, dashboardModule dashboard.Module, providerSettings factory.ProviderSettings, cfg metricsexplorer.Config) metricsexplorer.Module {
fieldMapper := telemetrymetrics.NewFieldMapper()
condBuilder := telemetrymetrics.NewConditionBuilder(fieldMapper)
return &module{
@@ -48,6 +50,7 @@ func NewModule(ts telemetrystore.TelemetryStore, telemetryMetadataStore telemetr
logger: providerSettings.Logger,
telemetryMetadataStore: telemetryMetadataStore,
cache: cache,
ruleStore: ruleStore,
dashboardModule: dashboardModule,
config: cfg,
}
@@ -197,11 +200,32 @@ func (m *module) UpdateMetricMetadata(ctx context.Context, orgID valuer.UUID, re
return nil
}
func (m *module) GetMetricAlerts(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricAlertsResponse, error) {
if metricName == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
}
ruleAlerts, err := m.ruleStore.GetStoredRulesByMetricName(ctx, orgID.String(), metricName)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get stored rules by metric name")
}
alerts := make([]metricsexplorertypes.MetricAlert, len(ruleAlerts))
for i, ruleAlert := range ruleAlerts {
alerts[i] = metricsexplorertypes.MetricAlert{
AlertName: ruleAlert.AlertName,
AlertID: ruleAlert.AlertID,
}
}
return &metricsexplorertypes.MetricAlertsResponse{
Alerts: alerts,
}, nil
}
func (m *module) GetMetricDashboards(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardsResponse, error) {
if metricName == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
}
data, err := m.dashboardModule.GetByMetricNames(ctx, orgID, []string{metricName})
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get dashboards for metric")

View File

@@ -15,6 +15,7 @@ type Handler interface {
GetMetricMetadata(http.ResponseWriter, *http.Request)
GetMetricAttributes(http.ResponseWriter, *http.Request)
UpdateMetricMetadata(http.ResponseWriter, *http.Request)
GetMetricAlerts(http.ResponseWriter, *http.Request)
GetMetricDashboards(http.ResponseWriter, *http.Request)
GetMetricHighlights(http.ResponseWriter, *http.Request)
}
@@ -25,6 +26,7 @@ type Module interface {
GetTreemap(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.TreemapRequest) (*metricsexplorertypes.TreemapResponse, error)
GetMetricMetadataMulti(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]*metricsexplorertypes.MetricMetadata, error)
UpdateMetricMetadata(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.UpdateMetricMetadataRequest) error
GetMetricAlerts(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricAlertsResponse, error)
GetMetricDashboards(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardsResponse, error)
GetMetricHighlights(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricHighlightsResponse, error)
GetMetricAttributes(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.MetricAttributesRequest) (*metricsexplorertypes.MetricAttributesResponse, error)

View File

@@ -630,6 +630,7 @@ func (ah *APIHandler) MetricExplorerRoutes(router *mux.Router, am *middleware.Au
router.HandleFunc("/api/v2/metrics/metadata", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricMetadata)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/metrics/{metric_name}/metadata", am.EditAccess(ah.Signoz.Handlers.MetricsExplorer.UpdateMetricMetadata)).Methods(http.MethodPost)
router.HandleFunc("/api/v2/metric/highlights", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricHighlights)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/metric/alerts", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricAlerts)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/metric/dashboards", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricDashboards)).Methods(http.MethodGet)
}

View File

@@ -3,13 +3,13 @@ package app
import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
_ "net/http/pprof" // http profiler
"slices"
"github.com/SigNoz/signoz/pkg/cache/memorycache"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
@@ -17,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -30,6 +31,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/app/opamp"
opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
@@ -37,10 +39,8 @@ import (
"github.com/rs/cors"
"github.com/soheilhy/cmux"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
@@ -107,7 +107,8 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
signoz.Prometheus,
signoz.Modules.OrgGetter,
signoz.Querier,
signoz.Instrumentation.Logger(),
signoz.Instrumentation.ToProviderSettings(),
signoz.QueryParser,
)
if err != nil {
return nil, err
@@ -339,9 +340,10 @@ func makeRulesManager(
prometheus prometheus.Prometheus,
orgGetter organization.Getter,
querier querier.Querier,
logger *slog.Logger,
providerSettings factory.ProviderSettings,
queryParser queryparser.QueryParser,
) (*rules.Manager, error) {
ruleStore := sqlrulestore.NewRuleStore(sqlstore)
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
// create manager opts
managerOpts := &rules.ManagerOptions{
@@ -351,7 +353,7 @@ func makeRulesManager(
Logger: zap.L(),
Reader: ch,
Querier: querier,
SLogger: logger,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: constants.GetEvalDelay(),
OrgGetter: orgGetter,

View File

@@ -5,6 +5,7 @@ import (
"regexp"
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
@@ -21,7 +22,9 @@ type MockSQLRuleStore struct {
// NewMockSQLRuleStore creates a new MockSQLRuleStore with sqlmock
func NewMockSQLRuleStore() *MockSQLRuleStore {
sqlStore := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherRegexp)
ruleStore := sqlrulestore.NewRuleStore(sqlStore)
// For tests, we can pass nil for queryParser and use test provider settings
providerSettings := factorytest.NewSettings()
ruleStore := sqlrulestore.NewRuleStore(sqlStore, nil, providerSettings)
return &MockSQLRuleStore{
ruleStore: ruleStore,
@@ -59,6 +62,11 @@ func (m *MockSQLRuleStore) GetStoredRules(ctx context.Context, orgID string) ([]
return m.ruleStore.GetStoredRules(ctx, orgID)
}
// GetStoredRulesByMetricName implements ruletypes.RuleStore - delegates to underlying ruleStore
func (m *MockSQLRuleStore) GetStoredRulesByMetricName(ctx context.Context, orgID string, metricName string) ([]ruletypes.RuleAlert, error) {
return m.ruleStore.GetStoredRulesByMetricName(ctx, orgID, metricName)
}
// ExpectCreateRule sets up SQL expectations for CreateRule operation
func (m *MockSQLRuleStore) ExpectCreateRule(rule *ruletypes.Rule) {
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "created_by", "updated_by", "deleted", "data", "org_id"}).
@@ -104,6 +112,17 @@ func (m *MockSQLRuleStore) ExpectGetStoredRules(orgID string, rules []*ruletypes
WillReturnRows(rows)
}
// ExpectGetStoredRulesByMetricName sets up SQL expectations for GetStoredRulesByMetricName operation
func (m *MockSQLRuleStore) ExpectGetStoredRulesByMetricName(orgID string, metricName string, rules []*ruletypes.Rule) {
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "created_by", "updated_by", "deleted", "data", "org_id"})
for _, rule := range rules {
rows.AddRow(rule.ID, rule.CreatedAt, rule.UpdatedAt, rule.CreatedBy, rule.UpdatedBy, rule.Deleted, rule.Data, rule.OrgID)
}
expectedPattern := `SELECT (.+) FROM "rule".+WHERE \(.+org_id.+'` + orgID + `'\)`
m.mock.ExpectQuery(expectedPattern).
WillReturnRows(rows)
}
// AssertExpectations asserts that all SQL expectations were met
func (m *MockSQLRuleStore) AssertExpectations() error {
return m.mock.ExpectationsWereMet()

View File

@@ -2,18 +2,31 @@ package sqlrulestore
import (
"context"
"encoding/json"
"log/slog"
"slices"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sqlstore"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type rule struct {
sqlstore sqlstore.SQLStore
sqlstore sqlstore.SQLStore
queryParser queryparser.QueryParser
logger *slog.Logger
}
func NewRuleStore(store sqlstore.SQLStore) ruletypes.RuleStore {
return &rule{sqlstore: store}
func NewRuleStore(store sqlstore.SQLStore, queryParser queryparser.QueryParser, providerSettings factory.ProviderSettings) ruletypes.RuleStore {
return &rule{
sqlstore: store,
queryParser: queryParser,
logger: providerSettings.Logger,
}
}
func (r *rule) CreateRule(ctx context.Context, storedRule *ruletypes.Rule, cb func(context.Context, valuer.UUID) error) (valuer.UUID, error) {
@@ -101,3 +114,92 @@ func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.Ru
}
return rule, nil
}
func (r *rule) GetStoredRulesByMetricName(ctx context.Context, orgID string, metricName string) ([]ruletypes.RuleAlert, error) {
if metricName == "" {
return []ruletypes.RuleAlert{}, nil
}
// Get all stored rules for the organization
storedRules, err := r.GetStoredRules(ctx, orgID)
if err != nil {
return nil, err
}
alerts := make([]ruletypes.RuleAlert, 0)
seen := make(map[string]bool)
for _, storedRule := range storedRules {
var ruleData ruletypes.PostableRule
if err := json.Unmarshal([]byte(storedRule.Data), &ruleData); err != nil {
r.logger.WarnContext(ctx, "failed to unmarshal rule data", "rule_id", storedRule.ID.StringValue(), "error", err)
continue
}
// Check conditions: must be metric-based alert with valid composite query
if ruleData.AlertType != ruletypes.AlertTypeMetric ||
ruleData.RuleCondition == nil ||
ruleData.RuleCondition.CompositeQuery == nil {
continue
}
// Search for metricName in the Queries array (v5 format only)
// TODO check if we need to support v3 query format structs
found := false
for _, queryEnvelope := range ruleData.RuleCondition.CompositeQuery.Queries {
// Check based on query type
switch queryEnvelope.Type {
case qbtypes.QueryTypeBuilder:
// Cast to QueryBuilderQuery[MetricAggregation] for metrics
if spec, ok := queryEnvelope.Spec.(qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]); ok {
// Check if signal is metrics
if spec.Signal == telemetrytypes.SignalMetrics {
for _, agg := range spec.Aggregations {
if agg.MetricName == metricName {
found = true
break
}
}
}
}
case qbtypes.QueryTypePromQL:
if spec, ok := queryEnvelope.Spec.(qbtypes.PromQuery); ok {
result, err := r.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypePromQL, spec.Query)
if err != nil {
r.logger.WarnContext(ctx, "failed to parse PromQL query", "query", spec.Query, "error", err)
continue
}
if slices.Contains(result.MetricNames, metricName) {
found = true
break
}
}
case qbtypes.QueryTypeClickHouseSQL:
if spec, ok := queryEnvelope.Spec.(qbtypes.ClickHouseQuery); ok {
result, err := r.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypeClickHouseSQL, spec.Query)
if err != nil {
r.logger.WarnContext(ctx, "failed to parse ClickHouse query", "query", spec.Query, "error", err)
continue
}
if slices.Contains(result.MetricNames, metricName) {
found = true
break
}
}
}
if found {
break
}
}
if found && !seen[storedRule.ID.StringValue()] {
seen[storedRule.ID.StringValue()] = true
alerts = append(alerts, ruletypes.RuleAlert{
AlertName: ruleData.AlertName,
AlertID: storedRule.ID.StringValue(),
})
}
}
return alerts, nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -22,7 +23,8 @@ func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[ruler.Ruler,
}
func New(ctx context.Context, settings factory.ProviderSettings, config ruler.Config, sqlstore sqlstore.SQLStore) (ruler.Ruler, error) {
return &provider{ruleStore: sqlrulestore.NewRuleStore(sqlstore)}, nil
queryParser := queryparser.New(settings)
return &provider{ruleStore: sqlrulestore.NewRuleStore(sqlstore, queryParser, settings)}, nil
}
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {

View File

@@ -39,6 +39,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"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/tokenizer"
@@ -87,6 +88,7 @@ func NewModules(
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics)
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
dashboard := impldashboard.NewModule(sqlstore, providerSettings, analytics, orgGetter, implrole.NewModule(implrole.NewStore(sqlstore), authz, nil), queryParser)
return Modules{
@@ -105,6 +107,6 @@ func NewModules(
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, dashboard, providerSettings, config.MetricsExplorer),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
}
}

View File

@@ -221,6 +221,17 @@ type TreemapResponse struct {
Samples []TreemapEntry `json:"samples"`
}
// MetricAlert represents an alert associated with a metric.
type MetricAlert struct {
AlertName string `json:"alertName"`
AlertID string `json:"alertId"`
}
// MetricAlertsResponse represents the response for metric alerts endpoint.
type MetricAlertsResponse struct {
Alerts []MetricAlert `json:"alerts"`
}
// MetricDashboard represents a dashboard/widget referencing a metric.
type MetricDashboard struct {
DashboardName string `json:"dashboardName"`

View File

@@ -47,10 +47,17 @@ func NewStatsFromRules(rules []*Rule) map[string]any {
return stats
}
// RuleAlert represents an alert associated with a rule, used when filtering by metric name
type RuleAlert struct {
AlertName string
AlertID string
}
type RuleStore interface {
CreateRule(context.Context, *Rule, func(context.Context, valuer.UUID) error) (valuer.UUID, error)
EditRule(context.Context, *Rule, func(context.Context) error) error
DeleteRule(context.Context, valuer.UUID, func(context.Context) error) error
GetStoredRules(context.Context, string) ([]*Rule, error)
GetStoredRule(context.Context, valuer.UUID) (*Rule, error)
GetStoredRulesByMetricName(context.Context, string, string) ([]RuleAlert, error)
}