diff --git a/pkg/query-service/app/apdex.go b/pkg/query-service/app/apdex.go new file mode 100644 index 0000000000..6854a91367 --- /dev/null +++ b/pkg/query-service/app/apdex.go @@ -0,0 +1,46 @@ +package app + +import ( + "context" + "net/http" + "strings" + + "go.signoz.io/signoz/pkg/query-service/dao" + "go.signoz.io/signoz/pkg/query-service/model" +) + +func (aH *APIHandler) setApdexSettings(w http.ResponseWriter, r *http.Request) { + req, err := parseSetApdexScoreRequest(r) + if aH.HandleError(w, err, http.StatusBadRequest) { + return + } + + if err := dao.DB().SetApdexSettings(context.Background(), req); err != nil { + RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + + aH.WriteJSON(w, r, map[string]string{"data": "apdex score updated successfully"}) +} + +func (aH *APIHandler) getApdexSettings(w http.ResponseWriter, r *http.Request) { + services := r.URL.Query().Get("services") + apdexSet, err := dao.DB().GetApdexSettings(context.Background(), strings.Split(strings.TrimSpace(services), ",")) + if err != nil { + RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + + aH.WriteJSON(w, r, apdexSet) +} + +func (aH *APIHandler) getLatencyMetricMetadata(w http.ResponseWriter, r *http.Request) { + metricName := r.URL.Query().Get("metricName") + metricMetadata, err := aH.reader.GetLatencyMetricMetadata(r.Context(), metricName, aH.preferDelta) + if err != nil { + RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + + aH.WriteJSON(w, r, metricMetadata) +} diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index ac763be193..5d4faae2e6 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -5,6 +5,7 @@ import ( "context" "database/sql" "encoding/json" + "math" "fmt" "io/ioutil" @@ -3829,6 +3830,52 @@ func (r *ClickHouseReader) GetMetricAttributeValues(ctx context.Context, req *v3 return &attributeValues, nil } +func (r *ClickHouseReader) GetLatencyMetricMetadata(ctx context.Context, metricName string, preferDelta bool) (*v3.LatencyMetricMetadataResponse, error) { + query := fmt.Sprintf("SELECT DISTINCT(temporality) from %s.%s WHERE metric_name='%s'", signozMetricDBName, signozTSTableName, metricName) + rows, err := r.db.Query(ctx, query, metricName) + if err != nil { + zap.S().Error(err) + return nil, fmt.Errorf("error while executing query: %s", err.Error()) + } + defer rows.Close() + + var deltaExists bool + for rows.Next() { + var temporality string + if err := rows.Scan(&temporality); err != nil { + return nil, fmt.Errorf("error while scanning rows: %s", err.Error()) + } + if temporality == string(v3.Delta) { + deltaExists = true + } + } + + query = fmt.Sprintf("SELECT DISTINCT(toFloat64(JSONExtractString(labels, 'le'))) as le from %s.%s WHERE metric_name='%s' ORDER BY le", signozMetricDBName, signozTSTableName, metricName) + rows, err = r.db.Query(ctx, query, metricName) + if err != nil { + zap.S().Error(err) + return nil, fmt.Errorf("error while executing query: %s", err.Error()) + } + defer rows.Close() + + var leFloat64 []float64 + for rows.Next() { + var le float64 + if err := rows.Scan(&le); err != nil { + return nil, fmt.Errorf("error while scanning rows: %s", err.Error()) + } + if math.IsInf(le, 0) { + continue + } + leFloat64 = append(leFloat64, le) + } + + return &v3.LatencyMetricMetadataResponse{ + Delta: deltaExists && preferDelta, + Le: leFloat64, + }, nil +} + func isColumn(tableStatement, field string) bool { return strings.Contains(tableStatement, fmt.Sprintf("`%s` ", field)) } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index e53e9af91a..5008d6edae 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -332,6 +332,10 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) { router.HandleFunc("/api/v1/dependency_graph", am.ViewAccess(aH.dependencyGraph)).Methods(http.MethodPost) router.HandleFunc("/api/v1/settings/ttl", am.AdminAccess(aH.setTTL)).Methods(http.MethodPost) router.HandleFunc("/api/v1/settings/ttl", am.ViewAccess(aH.getTTL)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/settings/apdex", am.AdminAccess(aH.setApdexSettings)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/settings/apdex", am.ViewAccess(aH.getApdexSettings)).Methods(http.MethodGet) + + router.HandleFunc("/api/v1/metric_meta", am.ViewAccess(aH.getLatencyMetricMetadata)).Methods(http.MethodGet) router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet) router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet) diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index 4804847ea2..5c44ba19f4 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -726,6 +726,14 @@ func parseInviteRequest(r *http.Request) (*model.InviteRequest, error) { return &req, nil } +func parseSetApdexScoreRequest(r *http.Request) (*model.ApdexSettings, error) { + var req model.ApdexSettings + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return &req, nil +} + func parseRegisterRequest(r *http.Request) (*auth.RegisterRequest, error) { var req auth.RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/pkg/query-service/dao/interface.go b/pkg/query-service/dao/interface.go index ceece7faef..c1bb852e20 100644 --- a/pkg/query-service/dao/interface.go +++ b/pkg/query-service/dao/interface.go @@ -32,6 +32,8 @@ type Queries interface { GetResetPasswordEntry(ctx context.Context, token string) (*model.ResetPasswordEntry, *model.ApiError) GetUsersByOrg(ctx context.Context, orgId string) ([]model.UserPayload, *model.ApiError) GetUsersByGroup(ctx context.Context, groupId string) ([]model.UserPayload, *model.ApiError) + + GetApdexSettings(ctx context.Context, services []string) ([]model.ApdexSettings, *model.ApiError) } type Mutations interface { @@ -56,4 +58,6 @@ type Mutations interface { UpdateUserPassword(ctx context.Context, hash, userId string) *model.ApiError UpdateUserGroup(ctx context.Context, userId, groupId string) *model.ApiError + + SetApdexSettings(ctx context.Context, set *model.ApdexSettings) *model.ApiError } diff --git a/pkg/query-service/dao/sqlite/apdex.go b/pkg/query-service/dao/sqlite/apdex.go new file mode 100644 index 0000000000..8c74553fb8 --- /dev/null +++ b/pkg/query-service/dao/sqlite/apdex.go @@ -0,0 +1,74 @@ +package sqlite + +import ( + "context" + "fmt" + + "go.signoz.io/signoz/pkg/query-service/model" +) + +const defaultApdexThreshold = 0.5 + +func (mds *ModelDaoSqlite) GetApdexSettings(ctx context.Context, services []string) ([]model.ApdexSettings, *model.ApiError) { + var apdexSettings []model.ApdexSettings + var serviceName string + + for i, service := range services { + if i == 0 { + serviceName = fmt.Sprintf("'%s'", service) + } else { + serviceName = fmt.Sprintf("%s, '%s'", serviceName, service) + } + } + + query := fmt.Sprintf("SELECT * FROM apdex_settings WHERE service_name IN (%s)", serviceName) + + err := mds.db.Select(&apdexSettings, query) + if err != nil { + return nil, &model.ApiError{ + Err: err, + } + } + + // add default apdex settings for services that don't have any + for _, service := range services { + var found bool + for _, apdexSetting := range apdexSettings { + if apdexSetting.ServiceName == service { + found = true + break + } + } + + if !found { + apdexSettings = append(apdexSettings, model.ApdexSettings{ + ServiceName: service, + Threshold: defaultApdexThreshold, + }) + } + } + + return apdexSettings, nil +} + +func (mds *ModelDaoSqlite) SetApdexSettings(ctx context.Context, apdexSettings *model.ApdexSettings) *model.ApiError { + + fmt.Println("apdexSettings:", apdexSettings) + _, err := mds.db.NamedExec(` + INSERT OR REPLACE INTO apdex_settings ( + service_name, + threshold, + exclude_status_codes + ) VALUES ( + :service_name, + :threshold, + :exclude_status_codes + )`, apdexSettings) + if err != nil { + return &model.ApiError{ + Err: err, + } + } + + return nil +} diff --git a/pkg/query-service/dao/sqlite/connection.go b/pkg/query-service/dao/sqlite/connection.go index a1b7842bb3..dd113a2863 100644 --- a/pkg/query-service/dao/sqlite/connection.go +++ b/pkg/query-service/dao/sqlite/connection.go @@ -73,6 +73,11 @@ func InitDB(dataSourceName string) (*ModelDaoSqlite, error) { flags TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ); + CREATE TABLE IF NOT EXISTS apdex_settings ( + service_name TEXT PRIMARY KEY, + threshold FLOAT NOT NULL, + exclude_status_codes TEXT NOT NULL + ); ` _, err = db.Exec(table_schema) diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index f53a3a1fe6..8b3697f89c 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -95,6 +95,8 @@ type Reader interface { QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error) CheckClickHouse(ctx context.Context) error + + GetLatencyMetricMetadata(context.Context, string, bool) (*v3.LatencyMetricMetadataResponse, error) } type Querier interface { diff --git a/pkg/query-service/model/db.go b/pkg/query-service/model/db.go index e043dd2ddd..9ae7270232 100644 --- a/pkg/query-service/model/db.go +++ b/pkg/query-service/model/db.go @@ -36,6 +36,12 @@ type User struct { GroupId string `json:"groupId,omitempty" db:"group_id"` } +type ApdexSettings struct { + ServiceName string `json:"serviceName" db:"service_name"` + Threshold float64 `json:"threshold" db:"threshold"` + ExcludeStatusCodes string `json:"excludeStatusCodes" db:"exclude_status_codes"` // sqlite doesn't support array type +} + type UserFlag map[string]string func (uf UserFlag) Value() (driver.Value, error) { diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index d1dab19c0d..4e4fd2c0a1 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -663,3 +663,8 @@ func (eq *ExplorerQuery) Validate() error { } return eq.CompositeQuery.Validate() } + +type LatencyMetricMetadataResponse struct { + Delta bool `json:"delta"` + Le []float64 `json:"le"` +}