mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-28 02:12:02 +08:00
293 lines
7.0 KiB
Go
293 lines
7.0 KiB
Go
package logs
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"go.signoz.io/query-service/model"
|
|
)
|
|
|
|
var operatorMapping = map[string]string{
|
|
"lt": "<",
|
|
"gt": ">",
|
|
"lte": "<=",
|
|
"gte": ">=",
|
|
"in": "IN",
|
|
"nin": "NOT IN",
|
|
"contains": "ILIKE",
|
|
"ncontains": "NOT ILIKE",
|
|
}
|
|
|
|
const (
|
|
AND = "and"
|
|
)
|
|
|
|
var tokenRegex, _ = regexp.Compile(`(?i)(and( )*?)?(([\w.-]+ (in|nin) \([\S ]+\))|([\w.]+ (gt|lt|gte|lte) (')?[\S]+(')?)|([\w.]+ (contains|ncontains)) (')?[\S ]+(')?)`)
|
|
var operatorRegex, _ = regexp.Compile(`(?i)(?: )(in|nin|gt|lt|gte|lte|contains|ncontains)(?: )`)
|
|
|
|
func ParseLogFilterParams(r *http.Request) (*model.LogsFilterParams, error) {
|
|
res := model.LogsFilterParams{
|
|
Limit: 30,
|
|
OrderBy: "timestamp",
|
|
Order: "desc",
|
|
}
|
|
var err error
|
|
params := r.URL.Query()
|
|
if val, ok := params["limit"]; ok {
|
|
res.Limit, err = strconv.Atoi(val[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if val, ok := params["orderBy"]; ok {
|
|
res.OrderBy = val[0]
|
|
}
|
|
if val, ok := params["order"]; ok {
|
|
res.Order = val[0]
|
|
}
|
|
if val, ok := params["q"]; ok {
|
|
res.Query = &val[0]
|
|
}
|
|
if val, ok := params["timestampStart"]; ok {
|
|
ts, err := strconv.Atoi(val[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ts64 := uint64(ts)
|
|
res.TimestampStart = &ts64
|
|
}
|
|
if val, ok := params["timestampEnd"]; ok {
|
|
ts, err := strconv.Atoi(val[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ts64 := uint64(ts)
|
|
res.TimestampEnd = &ts64
|
|
}
|
|
if val, ok := params["idStart"]; ok {
|
|
res.IdStart = &val[0]
|
|
}
|
|
if val, ok := params["idEnd"]; ok {
|
|
res.IdEnd = &val[0]
|
|
}
|
|
return &res, nil
|
|
}
|
|
|
|
func ParseLiveTailFilterParams(r *http.Request) (*model.LogsFilterParams, error) {
|
|
res := model.LogsFilterParams{}
|
|
params := r.URL.Query()
|
|
if val, ok := params["q"]; ok {
|
|
res.Query = &val[0]
|
|
}
|
|
if val, ok := params["timestampStart"]; ok {
|
|
ts, err := strconv.Atoi(val[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ts64 := uint64(ts)
|
|
res.TimestampStart = &ts64
|
|
}
|
|
if val, ok := params["idStart"]; ok {
|
|
res.IdStart = &val[0]
|
|
}
|
|
return &res, nil
|
|
}
|
|
|
|
func ParseLogAggregateParams(r *http.Request) (*model.LogsAggregateParams, error) {
|
|
res := model.LogsAggregateParams{}
|
|
params := r.URL.Query()
|
|
if val, ok := params["timestampStart"]; ok {
|
|
ts, err := strconv.Atoi(val[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ts64 := uint64(ts)
|
|
res.TimestampStart = &ts64
|
|
} else {
|
|
return nil, fmt.Errorf("timestampStart is required")
|
|
}
|
|
if val, ok := params["timestampEnd"]; ok {
|
|
ts, err := strconv.Atoi(val[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ts64 := uint64(ts)
|
|
res.TimestampEnd = &ts64
|
|
} else {
|
|
return nil, fmt.Errorf("timestampEnd is required")
|
|
}
|
|
|
|
if val, ok := params["q"]; ok {
|
|
res.Query = &val[0]
|
|
}
|
|
|
|
if val, ok := params["groupBy"]; ok {
|
|
res.GroupBy = &val[0]
|
|
}
|
|
|
|
if val, ok := params["function"]; ok {
|
|
res.Function = &val[0]
|
|
}
|
|
|
|
if val, ok := params["step"]; ok {
|
|
step, err := strconv.Atoi(val[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.StepSeconds = &step
|
|
} else {
|
|
return nil, fmt.Errorf("step is required")
|
|
}
|
|
return &res, nil
|
|
}
|
|
|
|
func parseLogQuery(query string) ([]string, error) {
|
|
sqlQueryTokens := []string{}
|
|
filterTokens := tokenRegex.FindAllString(query, -1)
|
|
|
|
if len(filterTokens) == 0 {
|
|
sqlQueryTokens = append(sqlQueryTokens, fmt.Sprintf("body ILIKE '%%%s%%' ", query))
|
|
return sqlQueryTokens, nil
|
|
}
|
|
|
|
// replace and check if there is something that is lying around
|
|
if len(strings.TrimSpace(tokenRegex.ReplaceAllString(query, ""))) > 0 {
|
|
return nil, fmt.Errorf("failed to parse query, contains unknown tokens")
|
|
}
|
|
|
|
for _, v := range filterTokens {
|
|
op := strings.TrimSpace(operatorRegex.FindString(v))
|
|
opLower := strings.ToLower(op)
|
|
|
|
if opLower == "contains" || opLower == "ncontains" {
|
|
searchString := strings.TrimSpace(strings.Split(v, op)[1])
|
|
|
|
operatorRemovedTokens := strings.Split(operatorRegex.ReplaceAllString(v, " "), " ")
|
|
searchCol := strings.ToLower(operatorRemovedTokens[0])
|
|
if searchCol == AND {
|
|
searchCol = strings.ToLower(operatorRemovedTokens[1])
|
|
}
|
|
col := searchCol
|
|
if strings.ToLower(searchCol) == "fulltext" {
|
|
col = "body"
|
|
}
|
|
|
|
f := fmt.Sprintf(`%s %s '%%%s%%' `, col, operatorMapping[opLower], searchString[1:len(searchString)-1])
|
|
if strings.HasPrefix(strings.ToLower(v), AND) {
|
|
f = "AND " + f
|
|
}
|
|
sqlQueryTokens = append(sqlQueryTokens, f)
|
|
} else {
|
|
symbol := operatorMapping[strings.ToLower(op)]
|
|
sqlQueryTokens = append(sqlQueryTokens, strings.Replace(v, " "+op+" ", " "+symbol+" ", 1)+" ")
|
|
}
|
|
}
|
|
|
|
return sqlQueryTokens, nil
|
|
}
|
|
|
|
func parseColumn(s string) (*string, error) {
|
|
s = strings.ToLower(s)
|
|
|
|
colName := ""
|
|
|
|
// if has and/or as prefix
|
|
filter := strings.Split(s, " ")
|
|
if len(filter) < 3 {
|
|
return nil, fmt.Errorf("incorrect filter")
|
|
}
|
|
|
|
if strings.HasPrefix(s, AND) {
|
|
colName = filter[1]
|
|
} else {
|
|
colName = filter[0]
|
|
}
|
|
|
|
return &colName, nil
|
|
}
|
|
|
|
func arrayToMap(fields []model.LogField) map[string]model.LogField {
|
|
res := map[string]model.LogField{}
|
|
for _, field := range fields {
|
|
res[field.Name] = field
|
|
}
|
|
return res
|
|
}
|
|
|
|
func replaceInterestingFields(allFields *model.GetFieldsResponse, queryTokens []string) ([]string, error) {
|
|
// check if cols
|
|
selectedFieldsLookup := arrayToMap(allFields.Selected)
|
|
interestingFieldLookup := arrayToMap(allFields.Interesting)
|
|
|
|
for index := 0; index < len(queryTokens); index++ {
|
|
queryToken := queryTokens[index]
|
|
col, err := parseColumn(queryToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sqlColName := *col
|
|
if _, ok := selectedFieldsLookup[*col]; !ok && *col != "body" {
|
|
if field, ok := interestingFieldLookup[*col]; ok {
|
|
sqlColName = fmt.Sprintf("%s_%s_value[indexOf(%s_%s_key, '%s')]", field.Type, strings.ToLower(field.DataType), field.Type, strings.ToLower(field.DataType), *col)
|
|
} else {
|
|
return nil, fmt.Errorf("field not found for filtering")
|
|
}
|
|
}
|
|
queryTokens[index] = strings.Replace(queryToken, *col, sqlColName, 1)
|
|
}
|
|
return queryTokens, nil
|
|
}
|
|
|
|
func GenerateSQLWhere(allFields *model.GetFieldsResponse, params *model.LogsFilterParams) (*string, error) {
|
|
var tokens []string
|
|
var err error
|
|
if params.Query != nil {
|
|
tokens, err = parseLogQuery(*params.Query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
tokens, err = replaceInterestingFields(allFields, tokens)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if params.TimestampStart != nil {
|
|
filter := fmt.Sprintf("timestamp >= '%d' ", *params.TimestampStart)
|
|
if len(tokens) > 0 {
|
|
filter = "and " + filter
|
|
}
|
|
tokens = append(tokens, filter)
|
|
}
|
|
if params.TimestampEnd != nil {
|
|
filter := fmt.Sprintf("timestamp <= '%d' ", *params.TimestampEnd)
|
|
if len(tokens) > 0 {
|
|
filter = "and " + filter
|
|
}
|
|
tokens = append(tokens, filter)
|
|
}
|
|
if params.IdStart != nil {
|
|
filter := fmt.Sprintf("id > '%v' ", *params.IdStart)
|
|
if len(tokens) > 0 {
|
|
filter = "and " + filter
|
|
}
|
|
tokens = append(tokens, filter)
|
|
}
|
|
if params.IdEnd != nil {
|
|
filter := fmt.Sprintf("id < '%v' ", *params.IdEnd)
|
|
if len(tokens) > 0 {
|
|
filter = "and " + filter
|
|
}
|
|
tokens = append(tokens, filter)
|
|
}
|
|
|
|
sqlWhere := strings.Join(tokens, "")
|
|
|
|
return &sqlWhere, nil
|
|
}
|