mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-10 03:32:04 +00:00
Compare commits
4 Commits
query-rang
...
lp-filter-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40956116dc | ||
|
|
b49a95b7b3 | ||
|
|
ae19bb2be2 | ||
|
|
749f52ff9d |
1056
docs/api/openapi.yml
1056
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
querierAPI "github.com/SigNoz/signoz/pkg/querier"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
@@ -22,8 +22,6 @@ import (
|
||||
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -58,6 +56,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
||||
Signoz: signoz,
|
||||
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
|
||||
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||
@@ -109,22 +108,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
||||
|
||||
// v5
|
||||
router.Handle("/api/v5/query_range", handler.New(am.ViewAccess(ah.queryRangeV5), handler.OpenAPIDef{
|
||||
ID: "QueryRangeV5",
|
||||
Tags: []string{"query"},
|
||||
Summary: "Query range",
|
||||
Description: "Execute a composite query over a time range. Supports builder queries (traces, logs, metrics), formulas, joins, trace operators, PromQL, and ClickHouse SQL.",
|
||||
Request: new(qbtypes.QueryRangeRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(qbtypes.QueryRangeResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{
|
||||
{Name: ctxtypes.AuthTypeAPIKey.StringValue(), Scopes: []string{"VIEWER"}},
|
||||
{Name: ctxtypes.AuthTypeTokenizer.StringValue(), Scopes: []string{"VIEWER"}},
|
||||
},
|
||||
})).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v5/query_range", am.ViewAccess(ah.queryRangeV5)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v5/substitute_vars", am.ViewAccess(ah.QuerierAPI.ReplaceVariables)).Methods(http.MethodPost)
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||
apiHandler.RegisterCloudIntegrationsRoutes(r, am)
|
||||
apiHandler.RegisterFieldsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterInfraMetricsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
|
||||
127
pkg/apis/fields/api.go
Normal file
127
pkg/apis/fields/api.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetadata"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
telemetryMetadataStore telemetrytypes.MetadataStore
|
||||
}
|
||||
|
||||
// TODO: move this to module and remove metastore init
|
||||
func NewAPI(
|
||||
settings factory.ProviderSettings,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
) *API {
|
||||
telemetryMetadataStore := telemetrymetadata.NewTelemetryMetaStore(
|
||||
settings,
|
||||
telemetryStore,
|
||||
telemetrytraces.DBName,
|
||||
telemetrytraces.TagAttributesV2TableName,
|
||||
telemetrytraces.SpanAttributesKeysTblName,
|
||||
telemetrytraces.SpanIndexV3TableName,
|
||||
telemetrymetrics.DBName,
|
||||
telemetrymetrics.AttributesMetadataTableName,
|
||||
telemetrymeter.DBName,
|
||||
telemetrymeter.SamplesAgg1dTableName,
|
||||
telemetrylogs.DBName,
|
||||
telemetrylogs.LogsV2TableName,
|
||||
telemetrylogs.TagAttributesV2TableName,
|
||||
telemetrylogs.LogAttributeKeysTblName,
|
||||
telemetrylogs.LogResourceKeysTblName,
|
||||
telemetrymetadata.DBName,
|
||||
telemetrymetadata.AttributesMetadataLocalTableName,
|
||||
)
|
||||
|
||||
return &API{
|
||||
telemetryStore: telemetryStore,
|
||||
telemetryMetadataStore: telemetryMetadataStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) GetFieldsKeys(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type fieldKeysResponse struct {
|
||||
Keys map[string][]*telemetrytypes.TelemetryFieldKey `json:"keys"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
bodyBytes, _ := io.ReadAll(r.Body)
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
ctx := r.Context()
|
||||
|
||||
fieldKeySelector, err := parseFieldKeyRequest(r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
keys, complete, err := api.telemetryMetadataStore.GetKeys(ctx, fieldKeySelector)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response := fieldKeysResponse{
|
||||
Keys: keys,
|
||||
Complete: complete,
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (api *API) GetFieldsValues(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type fieldValuesResponse struct {
|
||||
Values *telemetrytypes.TelemetryFieldValues `json:"values"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
bodyBytes, _ := io.ReadAll(r.Body)
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
ctx := r.Context()
|
||||
|
||||
fieldValueSelector, err := parseFieldValueRequest(r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
allValues, allComplete, err := api.telemetryMetadataStore.GetAllValues(ctx, fieldValueSelector)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
relatedValues, relatedComplete, err := api.telemetryMetadataStore.GetRelatedValues(ctx, fieldValueSelector)
|
||||
if err != nil {
|
||||
// we don't want to return error if we fail to get related values for some reason
|
||||
relatedValues = []string{}
|
||||
}
|
||||
|
||||
values := &telemetrytypes.TelemetryFieldValues{
|
||||
StringValues: allValues.StringValues,
|
||||
NumberValues: allValues.NumberValues,
|
||||
RelatedValues: relatedValues,
|
||||
}
|
||||
|
||||
response := fieldValuesResponse{
|
||||
Values: values,
|
||||
Complete: allComplete && relatedComplete,
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, response)
|
||||
}
|
||||
162
pkg/apis/fields/parse.go
Normal file
162
pkg/apis/fields/parse.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, error) {
|
||||
var req telemetrytypes.FieldKeySelector
|
||||
var signal telemetrytypes.Signal
|
||||
var source telemetrytypes.Source
|
||||
var err error
|
||||
|
||||
signalStr := r.URL.Query().Get("signal")
|
||||
if signalStr != "" {
|
||||
signal = telemetrytypes.Signal{String: valuer.NewString(signalStr)}
|
||||
} else {
|
||||
signal = telemetrytypes.SignalUnspecified
|
||||
}
|
||||
|
||||
sourceStr := r.URL.Query().Get("source")
|
||||
if sourceStr != "" {
|
||||
source = telemetrytypes.Source{String: valuer.NewString(sourceStr)}
|
||||
} else {
|
||||
source = telemetrytypes.SourceUnspecified
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("limit") != "" {
|
||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse limit")
|
||||
}
|
||||
req.Limit = limit
|
||||
} else {
|
||||
req.Limit = 1000
|
||||
}
|
||||
|
||||
var startUnixMilli, endUnixMilli int64
|
||||
|
||||
if r.URL.Query().Get("startUnixMilli") != "" {
|
||||
startUnixMilli, err = strconv.ParseInt(r.URL.Query().Get("startUnixMilli"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse startUnixMilli")
|
||||
}
|
||||
// Round down to the nearest 6 hours (21600000 milliseconds)
|
||||
startUnixMilli -= startUnixMilli % 21600000
|
||||
}
|
||||
if r.URL.Query().Get("endUnixMilli") != "" {
|
||||
endUnixMilli, err = strconv.ParseInt(r.URL.Query().Get("endUnixMilli"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse endUnixMilli")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse fieldContext directly instead of using JSON unmarshalling.
|
||||
var fieldContext telemetrytypes.FieldContext
|
||||
fieldContextStr := r.URL.Query().Get("fieldContext")
|
||||
if fieldContextStr != "" {
|
||||
fieldContext = telemetrytypes.FieldContext{String: valuer.NewString(fieldContextStr)}
|
||||
}
|
||||
|
||||
// Parse fieldDataType directly instead of using JSON unmarshalling.
|
||||
var fieldDataType telemetrytypes.FieldDataType
|
||||
fieldDataTypeStr := r.URL.Query().Get("fieldDataType")
|
||||
if fieldDataTypeStr != "" {
|
||||
fieldDataType = telemetrytypes.FieldDataType{String: valuer.NewString(fieldDataTypeStr)}
|
||||
}
|
||||
|
||||
metricName := r.URL.Query().Get("metricName")
|
||||
var metricContext *telemetrytypes.MetricContext
|
||||
if metricName != "" {
|
||||
metricContext = &telemetrytypes.MetricContext{
|
||||
MetricName: metricName,
|
||||
}
|
||||
}
|
||||
|
||||
name := r.URL.Query().Get("searchText")
|
||||
|
||||
if name != "" && fieldContext == telemetrytypes.FieldContextUnspecified {
|
||||
parsedFieldKey := telemetrytypes.GetFieldKeyFromKeyText(name)
|
||||
if parsedFieldKey.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
// Only apply inferred context if it is valid for the current signal
|
||||
if isContextValidForSignal(parsedFieldKey.FieldContext, signal) {
|
||||
name = parsedFieldKey.Name
|
||||
fieldContext = parsedFieldKey.FieldContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req = telemetrytypes.FieldKeySelector{
|
||||
StartUnixMilli: startUnixMilli,
|
||||
EndUnixMilli: endUnixMilli,
|
||||
Signal: signal,
|
||||
Source: source,
|
||||
Name: name,
|
||||
FieldContext: fieldContext,
|
||||
FieldDataType: fieldDataType,
|
||||
Limit: req.Limit,
|
||||
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
|
||||
MetricContext: metricContext,
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseFieldValueRequest(r *http.Request) (*telemetrytypes.FieldValueSelector, error) {
|
||||
keySelector, err := parseFieldKeyRequest(r)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse field key request")
|
||||
}
|
||||
|
||||
name := r.URL.Query().Get("name")
|
||||
if name != "" && keySelector.FieldContext == telemetrytypes.FieldContextUnspecified {
|
||||
parsedFieldKey := telemetrytypes.GetFieldKeyFromKeyText(name)
|
||||
if parsedFieldKey.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
// Only apply inferred context if it is valid for the current signal
|
||||
if isContextValidForSignal(parsedFieldKey.FieldContext, keySelector.Signal) {
|
||||
name = parsedFieldKey.Name
|
||||
keySelector.FieldContext = parsedFieldKey.FieldContext
|
||||
}
|
||||
}
|
||||
}
|
||||
keySelector.Name = name
|
||||
existingQuery := r.URL.Query().Get("existingQuery")
|
||||
value := r.URL.Query().Get("searchText")
|
||||
|
||||
// Parse limit for fieldValue request, fallback to default 50 if parsing fails.
|
||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if err != nil {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
req := telemetrytypes.FieldValueSelector{
|
||||
FieldKeySelector: keySelector,
|
||||
ExistingQuery: existingQuery,
|
||||
Value: value,
|
||||
Limit: limit,
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func isContextValidForSignal(ctx telemetrytypes.FieldContext, signal telemetrytypes.Signal) bool {
|
||||
if ctx == telemetrytypes.FieldContextResource ||
|
||||
ctx == telemetrytypes.FieldContextAttribute ||
|
||||
ctx == telemetrytypes.FieldContextScope {
|
||||
return true
|
||||
}
|
||||
|
||||
switch signal.StringValue() {
|
||||
case telemetrytypes.SignalLogs.StringValue():
|
||||
return ctx == telemetrytypes.FieldContextLog || ctx == telemetrytypes.FieldContextBody
|
||||
case telemetrytypes.SignalTraces.StringValue():
|
||||
return ctx == telemetrytypes.FieldContextSpan || ctx == telemetrytypes.FieldContextEvent || ctx == telemetrytypes.FieldContextTrace
|
||||
case telemetrytypes.SignalMetrics.StringValue():
|
||||
return ctx == telemetrytypes.FieldContextMetric
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addFieldsRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/fields/keys", handler.New(provider.authZ.ViewAccess(provider.fieldsHandler.GetFieldsKeys), handler.OpenAPIDef{
|
||||
ID: "GetFieldsKeys",
|
||||
Tags: []string{"fields"},
|
||||
Summary: "Get field keys",
|
||||
Description: "This endpoint returns field keys",
|
||||
Request: nil,
|
||||
RequestQuery: new(telemetrytypes.PostableFieldKeysParams),
|
||||
RequestContentType: "",
|
||||
Response: new(telemetrytypes.GettableFieldKeys),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/fields/values", handler.New(provider.authZ.ViewAccess(provider.fieldsHandler.GetFieldsValues), handler.OpenAPIDef{
|
||||
ID: "GetFieldsValues",
|
||||
Tags: []string{"fields"},
|
||||
Summary: "Get field values",
|
||||
Description: "This endpoint returns field values",
|
||||
Request: nil,
|
||||
RequestQuery: new(telemetrytypes.PostableFieldValueParams),
|
||||
RequestContentType: "",
|
||||
Response: new(telemetrytypes.GettableFieldValues),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
@@ -45,7 +44,6 @@ type provider struct {
|
||||
gatewayHandler gateway.Handler
|
||||
roleGetter role.Getter
|
||||
roleHandler role.Handler
|
||||
fieldsHandler fields.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -65,31 +63,9 @@ func NewFactory(
|
||||
gatewayHandler gateway.Handler,
|
||||
roleGetter role.Getter,
|
||||
roleHandler role.Handler,
|
||||
fieldsHandler fields.Handler,
|
||||
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
|
||||
return newProvider(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config,
|
||||
orgGetter,
|
||||
authz,
|
||||
orgHandler,
|
||||
userHandler,
|
||||
sessionHandler,
|
||||
authDomainHandler,
|
||||
preferenceHandler,
|
||||
globalHandler,
|
||||
promoteHandler,
|
||||
flaggerHandler,
|
||||
dashboardModule,
|
||||
dashboardHandler,
|
||||
metricsExplorerHandler,
|
||||
gatewayHandler,
|
||||
roleGetter,
|
||||
roleHandler,
|
||||
fieldsHandler,
|
||||
)
|
||||
return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, globalHandler, promoteHandler, flaggerHandler, dashboardModule, dashboardHandler, metricsExplorerHandler, gatewayHandler, roleGetter, roleHandler)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,7 +89,6 @@ func newProvider(
|
||||
gatewayHandler gateway.Handler,
|
||||
roleGetter role.Getter,
|
||||
roleHandler role.Handler,
|
||||
fieldsHandler fields.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
|
||||
router := mux.NewRouter().UseEncodedPath()
|
||||
@@ -136,7 +111,6 @@ func newProvider(
|
||||
gatewayHandler: gatewayHandler,
|
||||
roleGetter: roleGetter,
|
||||
roleHandler: roleHandler,
|
||||
fieldsHandler: fieldsHandler,
|
||||
}
|
||||
|
||||
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz, roleGetter)
|
||||
@@ -201,10 +175,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addFieldsRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package fields
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Handler interface {
|
||||
// Gets the fields keys for the given field key selector
|
||||
GetFieldsKeys(http.ResponseWriter, *http.Request)
|
||||
|
||||
// Gets the fields values for the given field value selector
|
||||
GetFieldsValues(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package implfields
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
telemetryMetadataStore telemetrytypes.MetadataStore
|
||||
}
|
||||
|
||||
func NewHandler(settings factory.ProviderSettings, telemetryMetadataStore telemetrytypes.MetadataStore) fields.Handler {
|
||||
return &handler{
|
||||
telemetryMetadataStore: telemetryMetadataStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *handler) GetFieldsKeys(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
|
||||
var params telemetrytypes.PostableFieldKeysParams
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), ¶ms); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
fieldKeySelector := telemetrytypes.NewFieldKeySelectorFromPostableFieldKeysParams(params)
|
||||
|
||||
keys, complete, err := handler.telemetryMetadataStore.GetKeys(ctx, fieldKeySelector)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, &telemetrytypes.GettableFieldKeys{
|
||||
Keys: keys,
|
||||
Complete: complete,
|
||||
})
|
||||
}
|
||||
|
||||
func (handler *handler) GetFieldsValues(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
|
||||
var params telemetrytypes.PostableFieldValueParams
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), ¶ms); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
fieldValueSelector := telemetrytypes.NewFieldValueSelectorFromPostableFieldValueParams(params)
|
||||
|
||||
allValues, allComplete, err := handler.telemetryMetadataStore.GetAllValues(ctx, fieldValueSelector)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
relatedValues, relatedComplete, err := handler.telemetryMetadataStore.GetRelatedValues(ctx, fieldValueSelector)
|
||||
if err != nil {
|
||||
// we don't want to return error if we fail to get related values for some reason
|
||||
relatedValues = []string{}
|
||||
}
|
||||
|
||||
values := &telemetrytypes.TelemetryFieldValues{
|
||||
StringValues: allValues.StringValues,
|
||||
NumberValues: allValues.NumberValues,
|
||||
RelatedValues: relatedValues,
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, &telemetrytypes.GettableFieldValues{
|
||||
Values: values,
|
||||
Complete: allComplete && relatedComplete,
|
||||
})
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
@@ -144,6 +145,8 @@ type APIHandler struct {
|
||||
|
||||
LicensingAPI licensing.API
|
||||
|
||||
FieldsAPI *fields.API
|
||||
|
||||
QuerierAPI *querierAPI.API
|
||||
|
||||
QueryParserAPI *queryparser.API
|
||||
@@ -174,6 +177,8 @@ type APIHandlerOpts struct {
|
||||
|
||||
LicensingAPI licensing.API
|
||||
|
||||
FieldsAPI *fields.API
|
||||
|
||||
QuerierAPI *querierAPI.API
|
||||
|
||||
QueryParserAPI *queryparser.API
|
||||
@@ -238,6 +243,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
|
||||
AlertmanagerAPI: opts.AlertmanagerAPI,
|
||||
LicensingAPI: opts.LicensingAPI,
|
||||
Signoz: opts.Signoz,
|
||||
FieldsAPI: opts.FieldsAPI,
|
||||
QuerierAPI: opts.QuerierAPI,
|
||||
QueryParserAPI: opts.QueryParserAPI,
|
||||
}
|
||||
@@ -393,6 +399,13 @@ func (aH *APIHandler) RegisterQueryRangeV3Routes(router *mux.Router, am *middlew
|
||||
subRouter.HandleFunc("/logs/livetail", am.ViewAccess(aH.QuerierAPI.QueryRawStream)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) RegisterFieldsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
subRouter := router.PathPrefix("/api/v1").Subrouter()
|
||||
|
||||
subRouter.HandleFunc("/fields/keys", am.ViewAccess(aH.FieldsAPI.GetFieldsKeys)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/fields/values", am.ViewAccess(aH.FieldsAPI.GetFieldsValues)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) RegisterInfraMetricsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
hostsSubRouter := router.PathPrefix("/api/v1/hosts").Subrouter()
|
||||
hostsSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getHostAttributeKeys)).Methods(http.MethodGet)
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -96,10 +96,10 @@ var buildProcessorTestData = []struct {
|
||||
|
||||
func TestBuildLogParsingProcessors(t *testing.T) {
|
||||
for _, test := range buildProcessorTestData {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
err := updateProcessorConfigsInCollectorConf(test.agentConf, test.pipelineProcessor)
|
||||
So(err, ShouldBeNil)
|
||||
So(test.agentConf, ShouldResemble, test.outputConf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.outputConf, test.agentConf)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -202,11 +202,11 @@ var BuildLogsPipelineTestData = []struct {
|
||||
|
||||
func TestBuildLogsPipeline(t *testing.T) {
|
||||
for _, test := range BuildLogsPipelineTestData {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
v, err := buildCollectorPipelineProcessorsList(test.currentPipeline, test.logsPipeline)
|
||||
So(err, ShouldBeNil)
|
||||
assert.NoError(t, err)
|
||||
fmt.Println(test.Name, "\n", test.currentPipeline, "\n", v, "\n", test.expectedPipeline)
|
||||
So(v, ShouldResemble, test.expectedPipeline)
|
||||
assert.Equal(t, test.expectedPipeline, v)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -239,19 +239,8 @@ func TestPipelineAliasCollisionsDontResultInDuplicateCollectorProcessors(t *test
|
||||
Alias: alias,
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -310,19 +299,8 @@ func TestPipelineRouterWorksEvenIfFirstOpIsDisabled(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -383,19 +361,8 @@ func TestPipeCharInAliasDoesntBreakCollectorConfig(t *testing.T) {
|
||||
Alias: "test|pipeline",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
|
||||
@@ -140,15 +140,8 @@ func (ic *LogParsingPipelineController) getDefaultPipelines() ([]pipelinetypes.G
|
||||
Alias: "NormalizeBodyDefault",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "body",
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "body EXISTS",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
|
||||
signozstanzahelper "github.com/SigNoz/signoz-otel-collector/processor/signozlogspipelineprocessor/stanza/operator/helper"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -203,10 +203,10 @@ var prepareProcessorTestData = []struct {
|
||||
|
||||
func TestPreparePipelineProcessor(t *testing.T) {
|
||||
for _, test := range prepareProcessorTestData {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
res, err := getOperators(test.Operators)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldResemble, test.Output)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.Output, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -214,19 +214,8 @@ func TestPreparePipelineProcessor(t *testing.T) {
|
||||
func TestNoCollectorErrorsFromProcessorsForMismatchedLogs(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
testPipelineFilter := &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
testPipelineFilter := &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
}
|
||||
makeTestPipeline := func(config []pipelinetypes.PipelineOperator) pipelinetypes.GettablePipeline {
|
||||
return pipelinetypes.GettablePipeline{
|
||||
@@ -470,19 +459,8 @@ func TestResourceFiltersWork(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "nginx",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "resource.service = 'nginx'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -524,11 +502,11 @@ func TestResourceFiltersWork(t *testing.T) {
|
||||
func TestPipelineFilterWithStringOpsShouldNotSpamWarningsIfAttributeIsMissing(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
for _, operator := range []v3.FilterOperator{
|
||||
v3.FilterOperatorContains,
|
||||
v3.FilterOperatorNotContains,
|
||||
v3.FilterOperatorRegex,
|
||||
v3.FilterOperatorNotRegex,
|
||||
for _, operator := range []string{
|
||||
"CONTAINS",
|
||||
"NOT CONTAINS",
|
||||
"REGEXP",
|
||||
"NOT REGEXP",
|
||||
} {
|
||||
testPipeline := pipelinetypes.GettablePipeline{
|
||||
StoreablePipeline: pipelinetypes.StoreablePipeline{
|
||||
@@ -540,19 +518,8 @@ func TestPipelineFilterWithStringOpsShouldNotSpamWarningsIfAttributeIsMissing(t
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: operator,
|
||||
Value: "nginx",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: fmt.Sprintf("resource.service %s 'nginx'", operator),
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -601,19 +568,8 @@ func TestAttributePathsContainingDollarDoNotBreakCollector(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "$test",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "test",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.$test = 'test'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -664,19 +620,8 @@ func TestMembershipOpInProcessorFieldExpressions(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "http.method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.http.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -773,18 +718,8 @@ func TestContainsFilterIsCaseInsensitive(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "body",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeUnspecified,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "contains",
|
||||
Value: "log",
|
||||
}},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "body CONTAINS 'log'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -806,18 +741,8 @@ func TestContainsFilterIsCaseInsensitive(t *testing.T) {
|
||||
Alias: "pipeline2",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "body",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeUnspecified,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "ncontains",
|
||||
Value: "ecom",
|
||||
}},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "body NCONTAINS 'ecom'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -25,19 +25,8 @@ func TestPipelinePreview(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -58,19 +47,8 @@ func TestPipelinePreview(t *testing.T) {
|
||||
Alias: "pipeline2",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
@@ -159,19 +137,8 @@ func TestGrokParsingProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -30,19 +30,8 @@ func TestRegexProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -97,19 +86,8 @@ func TestGrokProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -164,19 +142,8 @@ func TestJSONProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -230,20 +197,9 @@ func TestTraceParsingProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
}
|
||||
@@ -339,19 +295,8 @@ func TestAddProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -404,19 +349,8 @@ func TestRemoveProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -469,19 +403,8 @@ func TestCopyProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -535,19 +458,8 @@ func TestMoveProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -23,19 +23,8 @@ func TestSeverityParsingProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
@@ -141,19 +130,8 @@ func TestSeverityParsingProcessor(t *testing.T) {
|
||||
func TestNoCollectorErrorsFromSeverityParserForMismatchedLogs(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
testPipelineFilter := &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
testPipelineFilter := &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
}
|
||||
makeTestPipeline := func(config []pipelinetypes.PipelineOperator) pipelinetypes.GettablePipeline {
|
||||
return pipelinetypes.GettablePipeline{
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -24,19 +24,8 @@ func TestTimestampParsingProcessor(t *testing.T) {
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "attribute.method = 'GET'",
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{},
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/gorilla/handlers"
|
||||
|
||||
"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"
|
||||
@@ -132,6 +133,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
FluxInterval: config.Querier.FluxInterval,
|
||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||
LicensingAPI: nooplicensing.NewLicenseAPI(),
|
||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
||||
Signoz: signoz,
|
||||
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
|
||||
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||
@@ -213,6 +215,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
|
||||
api.RegisterLogsRoutes(r, am)
|
||||
api.RegisterIntegrationRoutes(r, am)
|
||||
api.RegisterCloudIntegrationsRoutes(r, am)
|
||||
api.RegisterFieldsRoutes(r, am)
|
||||
api.RegisterQueryRangeV3Routes(r, am)
|
||||
api.RegisterInfraMetricsRoutes(r, am)
|
||||
api.RegisterWebSocketPaths(r, am)
|
||||
|
||||
@@ -2,11 +2,16 @@ package queryBuilderToExpr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
expr "github.com/antonmedv/expr"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -15,119 +20,220 @@ var (
|
||||
CodeExprCompilationFailed = errors.MustNewCode("expr_compilation_failed")
|
||||
)
|
||||
|
||||
var logOperatorsToExpr = map[v3.FilterOperator]string{
|
||||
v3.FilterOperatorEqual: "==",
|
||||
v3.FilterOperatorNotEqual: "!=",
|
||||
v3.FilterOperatorLessThan: "<",
|
||||
v3.FilterOperatorLessThanOrEq: "<=",
|
||||
v3.FilterOperatorGreaterThan: ">",
|
||||
v3.FilterOperatorGreaterThanOrEq: ">=",
|
||||
v3.FilterOperatorContains: "contains",
|
||||
v3.FilterOperatorNotContains: "not contains",
|
||||
v3.FilterOperatorRegex: "matches",
|
||||
v3.FilterOperatorNotRegex: "not matches",
|
||||
v3.FilterOperatorIn: "in",
|
||||
v3.FilterOperatorNotIn: "not in",
|
||||
v3.FilterOperatorExists: "in",
|
||||
v3.FilterOperatorNotExists: "not in",
|
||||
// we dont support like and nlike as of now.
|
||||
var logOperatorsToExpr = map[qbtypes.FilterOperator]string{
|
||||
qbtypes.FilterOperatorEqual: "==",
|
||||
qbtypes.FilterOperatorNotEqual: "!=",
|
||||
qbtypes.FilterOperatorLessThan: "<",
|
||||
qbtypes.FilterOperatorLessThanOrEq: "<=",
|
||||
qbtypes.FilterOperatorGreaterThan: ">",
|
||||
qbtypes.FilterOperatorGreaterThanOrEq: ">=",
|
||||
qbtypes.FilterOperatorContains: "contains",
|
||||
qbtypes.FilterOperatorNotContains: "not contains",
|
||||
qbtypes.FilterOperatorRegexp: "matches",
|
||||
qbtypes.FilterOperatorNotRegexp: "not matches",
|
||||
qbtypes.FilterOperatorIn: "in",
|
||||
qbtypes.FilterOperatorNotIn: "not in",
|
||||
qbtypes.FilterOperatorExists: "in",
|
||||
qbtypes.FilterOperatorNotExists: "not in",
|
||||
// nlike and like are not supported yet
|
||||
}
|
||||
|
||||
func getName(v v3.AttributeKey) string {
|
||||
if v.Type == v3.AttributeKeyTypeTag {
|
||||
return fmt.Sprintf(`attributes["%s"]`, v.Key)
|
||||
} else if v.Type == v3.AttributeKeyTypeResource {
|
||||
return fmt.Sprintf(`resource["%s"]`, v.Key)
|
||||
func getName(key *telemetrytypes.TelemetryFieldKey) string {
|
||||
if key == nil {
|
||||
return ""
|
||||
}
|
||||
return v.Key
|
||||
}
|
||||
|
||||
func getTypeName(v v3.AttributeKeyType) string {
|
||||
if v == v3.AttributeKeyTypeTag {
|
||||
return "attributes"
|
||||
} else if v == v3.AttributeKeyTypeResource {
|
||||
return "resource"
|
||||
switch key.FieldContext {
|
||||
case telemetrytypes.FieldContextAttribute:
|
||||
return fmt.Sprintf(`attributes["%s"]`, key.Name)
|
||||
case telemetrytypes.FieldContextResource:
|
||||
return fmt.Sprintf(`resource["%s"]`, key.Name)
|
||||
case telemetrytypes.FieldContextBody:
|
||||
return fmt.Sprintf("%s.%s", key.FieldContext.StringValue(), key.Name)
|
||||
default:
|
||||
return key.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func Parse(filters *v3.FilterSet) (string, error) {
|
||||
var res []string
|
||||
for _, v := range filters.Items {
|
||||
if _, ok := logOperatorsToExpr[v.Operator]; !ok {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "operator not supported: %s", v.Operator)
|
||||
}
|
||||
func parseCondition(c qbtypes.FilterCondition) (string, error) {
|
||||
if len(c.Keys) == 0 {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "condition has no keys")
|
||||
}
|
||||
key := c.Keys[0]
|
||||
if _, ok := logOperatorsToExpr[c.Op]; !ok {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "operator not supported: %d", c.Op)
|
||||
}
|
||||
|
||||
name := getName(v.Key)
|
||||
value := exprFormattedValue(c.Value)
|
||||
var filter string
|
||||
|
||||
var filter string
|
||||
switch c.Op {
|
||||
case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists:
|
||||
// EXISTS/NOT EXISTS checks are special:
|
||||
// - For body fields, we check membership in fromJSON(body) using the JSON field name.
|
||||
// - For attribute/resource fields, we check membership in the appropriate map
|
||||
// ("attributes" or "resource") using the logical field name.
|
||||
// - For intrinsic / top‑level fields (no explicit context), we fall back to
|
||||
// equality against nil (see default case below).
|
||||
switch key.FieldContext {
|
||||
case telemetrytypes.FieldContextBody:
|
||||
// if body is a string and is a valid JSON, then check if the key exists in the JSON
|
||||
quoted := exprFormattedValue(key.Name)
|
||||
jsonMembership := fmt.Sprintf(
|
||||
`((type(body) == "string" && isJSON(body)) && %s %s %s)`,
|
||||
quoted, logOperatorsToExpr[c.Op], "fromJSON(body)",
|
||||
)
|
||||
|
||||
switch v.Operator {
|
||||
// uncomment following lines when new version of expr is used
|
||||
// case v3.FilterOperatorIn, v3.FilterOperatorNotIn:
|
||||
// filter = fmt.Sprintf("%s %s list%s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value))
|
||||
|
||||
case v3.FilterOperatorExists, v3.FilterOperatorNotExists:
|
||||
// accustom log filters like `body.log.message EXISTS` into EXPR language
|
||||
// where User is attempting to check for keys present in JSON log body
|
||||
if strings.HasPrefix(v.Key.Key, "body.") {
|
||||
// if body is a string and is a valid JSON, then check if the key exists in the JSON
|
||||
filter = fmt.Sprintf(`((type(body) == "string" && isJSON(body)) && %s %s %s)`, exprFormattedValue(strings.TrimPrefix(v.Key.Key, "body.")), logOperatorsToExpr[v.Operator], "fromJSON(body)")
|
||||
|
||||
// if body is a map, then check if the key exists in the map
|
||||
operator := v3.FilterOperatorNotEqual
|
||||
if v.Operator == v3.FilterOperatorNotExists {
|
||||
operator = v3.FilterOperatorEqual
|
||||
}
|
||||
nilCheckFilter := fmt.Sprintf("%s %s nil", v.Key.Key, logOperatorsToExpr[operator])
|
||||
|
||||
// join the two filters with OR
|
||||
filter = fmt.Sprintf(`(%s or (type(body) == "map" && (%s)))`, filter, nilCheckFilter)
|
||||
} else if typ := getTypeName(v.Key.Type); typ != "" {
|
||||
filter = fmt.Sprintf("%s %s %s", exprFormattedValue(v.Key.Key), logOperatorsToExpr[v.Operator], typ)
|
||||
} else {
|
||||
// if type of key is not available; is considered as TOP LEVEL key in OTEL Log Data model hence
|
||||
// switch Exist and Not Exists operators with NOT EQUAL and EQUAL respectively
|
||||
operator := v3.FilterOperatorNotEqual
|
||||
if v.Operator == v3.FilterOperatorNotExists {
|
||||
operator = v3.FilterOperatorEqual
|
||||
}
|
||||
|
||||
filter = fmt.Sprintf("%s %s nil", v.Key.Key, logOperatorsToExpr[operator])
|
||||
// if body is a map, then check if the key exists in the map
|
||||
operator := qbtypes.FilterOperatorNotEqual
|
||||
if c.Op == qbtypes.FilterOperatorNotExists {
|
||||
operator = qbtypes.FilterOperatorEqual
|
||||
}
|
||||
nilCheckFilter := fmt.Sprintf("%s.%s %s nil", key.FieldContext.StringValue(), key.Name, logOperatorsToExpr[operator])
|
||||
|
||||
// join the two filters with OR
|
||||
filter = fmt.Sprintf(`(%s or (type(body) == "map" && (%s)))`, jsonMembership, nilCheckFilter)
|
||||
case telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextResource:
|
||||
// Example: "http.method" in attributes
|
||||
target := "resource"
|
||||
if key.FieldContext == telemetrytypes.FieldContextAttribute {
|
||||
target = "attributes"
|
||||
}
|
||||
filter = fmt.Sprintf("%q %s %s", key.Name, logOperatorsToExpr[c.Op], target)
|
||||
default:
|
||||
filter = fmt.Sprintf("%s %s %s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value))
|
||||
|
||||
if v.Operator == v3.FilterOperatorContains || v.Operator == v3.FilterOperatorNotContains {
|
||||
// `contains` and `ncontains` should be case insensitive to match how they work when querying logs.
|
||||
filter = fmt.Sprintf(
|
||||
"lower(%s) %s lower(%s)",
|
||||
name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value),
|
||||
)
|
||||
}
|
||||
|
||||
// Avoid running operators on nil values
|
||||
if v.Operator != v3.FilterOperatorEqual && v.Operator != v3.FilterOperatorNotEqual {
|
||||
filter = fmt.Sprintf("%s != nil && %s", name, filter)
|
||||
// if type of key is not available; is considered as TOP LEVEL key in OTEL Log Data model hence
|
||||
// switch Exist and Not Exists operators with NOT EQUAL and EQUAL respectively
|
||||
operator := qbtypes.FilterOperatorNotEqual
|
||||
if c.Op == qbtypes.FilterOperatorNotExists {
|
||||
operator = qbtypes.FilterOperatorEqual
|
||||
}
|
||||
filter = fmt.Sprintf("%s %s nil", key.Name, logOperatorsToExpr[operator])
|
||||
}
|
||||
default:
|
||||
filter = fmt.Sprintf("%s %s %s", getName(key), logOperatorsToExpr[c.Op], value)
|
||||
if c.Op == qbtypes.FilterOperatorContains || c.Op == qbtypes.FilterOperatorNotContains {
|
||||
// `contains` and `ncontains` should be case insensitive to match how they work when querying logs.
|
||||
filter = fmt.Sprintf(
|
||||
"lower(%s) %s lower(%s)",
|
||||
getName(key), logOperatorsToExpr[c.Op], value,
|
||||
)
|
||||
}
|
||||
|
||||
// check if the filter is a correct expression language
|
||||
_, err := expr.Compile(filter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
// Avoid running operators on nil values
|
||||
if c.Op != qbtypes.FilterOperatorEqual && c.Op != qbtypes.FilterOperatorNotEqual {
|
||||
filter = fmt.Sprintf("%s != nil && %s", getName(key), filter)
|
||||
}
|
||||
res = append(res, filter)
|
||||
}
|
||||
|
||||
// check the final filter
|
||||
q := strings.Join(res, " "+strings.ToLower(filters.Operator)+" ")
|
||||
_, err := expr.Compile(q)
|
||||
_, err := expr.Compile(filter)
|
||||
if err != nil {
|
||||
return "", errors.WrapInternalf(err, CodeExprCompilationFailed, "failed to compile expression: %s", q)
|
||||
return "", err
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
// Parse converts the QB filter Expression (query builder expression string) into
|
||||
// the Expr expression string used by the collector. It parses the QB expression
|
||||
// into a FilterExprNode tree, then serializes that tree to the Expr dialect.
|
||||
func Parse(filter *qbtypes.Filter) (string, error) {
|
||||
if filter == nil || strings.TrimSpace(filter.Expression) == "" {
|
||||
return "", nil
|
||||
}
|
||||
node, err := querybuilder.ExtractFilterExprTree(filter.Expression)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if node == nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter expression; node found nil")
|
||||
}
|
||||
|
||||
return q, nil
|
||||
for _, condition := range node.Flatten() {
|
||||
for _, key := range condition.Keys {
|
||||
_, found := telemetrylogs.IntrinsicFields[key.Name]
|
||||
if key.FieldContext == telemetrytypes.FieldContextUnspecified && !found {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "field %q in filter expression must include a context prefix (attribute., resource., body.) OR can be one of the following fields: %v", key.Name, maps.Keys(telemetrylogs.IntrinsicFields))
|
||||
}
|
||||
}
|
||||
if condition.Op == qbtypes.FilterOperatorRegexp || condition.Op == qbtypes.FilterOperatorNotRegexp {
|
||||
switch condition.Value.(type) {
|
||||
case string:
|
||||
if _, err := regexp.Compile(condition.Value.(string)); err != nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "value for regex operator must be a valid regex")
|
||||
}
|
||||
default:
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "value for regex operator must be a string or a slice of strings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodeToExpr(node)
|
||||
}
|
||||
|
||||
func nodeToExpr(node *qbtypes.FilterExprNode) (string, error) {
|
||||
if node == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
switch node.Op {
|
||||
case qbtypes.LogicalOpLeaf:
|
||||
var parts []string
|
||||
for _, c := range node.Conditions {
|
||||
s, err := parseCondition(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parts = append(parts, s)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
// For a simple leaf, just join conditions with AND without wrapping
|
||||
// the whole clause in parentheses
|
||||
return strings.Join(parts, " and "), nil
|
||||
case qbtypes.LogicalOpAnd:
|
||||
var parts []string
|
||||
for _, child := range node.Children {
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
s, err := nodeToExpr(child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// When mixing AND/OR, we need parentheses around any OR child to
|
||||
// preserve the intended precedence: (a and (b or c)).
|
||||
if child.Op == qbtypes.LogicalOpOr {
|
||||
s = fmt.Sprintf("(%s)", s)
|
||||
}
|
||||
parts = append(parts, s)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return strings.Join(parts, " and "), nil
|
||||
case qbtypes.LogicalOpOr:
|
||||
var parts []string
|
||||
for _, child := range node.Children {
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
s, err := nodeToExpr(child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// When mixing AND/OR, we need parentheses around any AND child to
|
||||
// preserve the intended precedence: ((a and b) or c).
|
||||
if child.Op == qbtypes.LogicalOpAnd {
|
||||
s = fmt.Sprintf("(%s)", s)
|
||||
}
|
||||
parts = append(parts, s)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return strings.Join(parts, " or "), nil
|
||||
default:
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported logical op: %s", node.Op)
|
||||
}
|
||||
}
|
||||
|
||||
func exprFormattedValue(v interface{}) string {
|
||||
@@ -135,13 +241,12 @@ func exprFormattedValue(v interface{}) string {
|
||||
case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64:
|
||||
return fmt.Sprintf("%d", x)
|
||||
case float32, float64:
|
||||
return fmt.Sprintf("%f", x)
|
||||
return fmt.Sprintf("%v", x)
|
||||
case string:
|
||||
return fmt.Sprintf("\"%s\"", quoteEscapedString(x))
|
||||
case bool:
|
||||
return fmt.Sprintf("%v", x)
|
||||
|
||||
case []interface{}:
|
||||
case []any:
|
||||
if len(x) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
signozstanzahelper "github.com/SigNoz/signoz-otel-collector/processor/signozlogspipelineprocessor/stanza/operator/helper"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/expr-lang/expr/vm"
|
||||
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -13,183 +13,177 @@ import (
|
||||
func TestParseExpression(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
Name string
|
||||
Query *v3.FilterSet
|
||||
Query *qbtypes.Filter
|
||||
Expr string
|
||||
ExpectError bool
|
||||
}{
|
||||
{
|
||||
Name: "equal",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "checkbody", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key = 'checkbody'",
|
||||
},
|
||||
Expr: `attributes["key"] == "checkbody"`,
|
||||
},
|
||||
{
|
||||
Name: "not equal",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "checkbody", Operator: "!="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key != 'checkbody'",
|
||||
},
|
||||
Expr: `attributes["key"] != "checkbody"`,
|
||||
},
|
||||
{
|
||||
Name: "less than",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: "<"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key < 10",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] < 10`,
|
||||
},
|
||||
{
|
||||
Name: "greater than",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: ">"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key > 10",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] > 10`,
|
||||
},
|
||||
{
|
||||
Name: "less than equal",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: "<="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key <= 10",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] <= 10`,
|
||||
},
|
||||
{
|
||||
Name: "greater than equal",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: ">="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key >= 10",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] >= 10`,
|
||||
},
|
||||
// case sensitive
|
||||
{
|
||||
Name: "body contains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body contains 'checkbody'",
|
||||
},
|
||||
Expr: `body != nil && lower(body) contains lower("checkbody")`,
|
||||
},
|
||||
{
|
||||
Name: "body.log.message exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.log.message", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.log.message exists",
|
||||
},
|
||||
Expr: `(((type(body) == "string" && isJSON(body)) && "log.message" in fromJSON(body)) or (type(body) == "map" && (body.log.message != nil)))`,
|
||||
},
|
||||
{
|
||||
Name: "body.log.message not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.log.message", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.log.message not exists",
|
||||
},
|
||||
Expr: `(((type(body) == "string" && isJSON(body)) && "log.message" not in fromJSON(body)) or (type(body) == "map" && (body.log.message == nil)))`,
|
||||
},
|
||||
{
|
||||
Name: "body ncontains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "ncontains"},
|
||||
}},
|
||||
Name: "body NOT CONTAINS",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body NOT CONTAINS 'checkbody'",
|
||||
},
|
||||
Expr: `body != nil && lower(body) not contains lower("checkbody")`,
|
||||
},
|
||||
{
|
||||
Name: "body regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex$", Operator: "regex"},
|
||||
}},
|
||||
Name: "body REGEXP",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body REGEXP '[0-1]+regex$'",
|
||||
},
|
||||
Expr: `body != nil && body matches "[0-1]+regex$"`,
|
||||
},
|
||||
{
|
||||
Name: "body not regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex$", Operator: "nregex"},
|
||||
}},
|
||||
Name: "body NOT REGEXP",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body NOT REGEXP '[0-1]+regex$'",
|
||||
},
|
||||
Expr: `body != nil && body not matches "[0-1]+regex$"`,
|
||||
},
|
||||
{
|
||||
Name: "regex with escape characters",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: `^Executing \[\S+@\S+:[0-9]+\] \S+".*`, Operator: "regex"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body REGEXP '^Executing \\[\\S+@\\S+:[0-9]+\\] \\S+\".*'",
|
||||
},
|
||||
Expr: `body != nil && body matches "^Executing \\[\\S+@\\S+:[0-9]+\\] \\S+\".*"`,
|
||||
},
|
||||
{
|
||||
Name: "invalid regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-9]++", Operator: "nregex"},
|
||||
}},
|
||||
Expr: `body != nil && lower(body) not matches "[0-9]++"`,
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body not REGEXP '[0-9]++'",
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "in",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: []any{1, 2, 3, 4}, Operator: "in"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key in [1,2,3,4]",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] in [1,2,3,4]`,
|
||||
},
|
||||
{
|
||||
Name: "not in",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: []any{"1", "2"}, Operator: "nin"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key not in ['1','2']",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] not in ['1','2']`,
|
||||
},
|
||||
{
|
||||
Name: "exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key exists",
|
||||
},
|
||||
Expr: `"key" in attributes`,
|
||||
},
|
||||
{
|
||||
Name: "not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key not exists",
|
||||
},
|
||||
Expr: `"key" not in attributes`,
|
||||
},
|
||||
{
|
||||
Name: "trace_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id not exists",
|
||||
},
|
||||
Expr: `trace_id == nil`,
|
||||
},
|
||||
{
|
||||
Name: "trace_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id exists",
|
||||
},
|
||||
Expr: `trace_id != nil`,
|
||||
},
|
||||
{
|
||||
Name: "span_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id not exists",
|
||||
},
|
||||
Expr: `span_id == nil`,
|
||||
},
|
||||
{
|
||||
Name: "span_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id exists",
|
||||
},
|
||||
Expr: `span_id != nil`,
|
||||
},
|
||||
{
|
||||
Name: "Multi filter",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: "<="},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex$", Operator: "nregex"},
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key <= 10 and body not regexp '[0-1]+regex$' and attribute.key not exists",
|
||||
},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] <= 10 and body != nil && body not matches "[0-1]+regex$" and "key" not in attributes`,
|
||||
},
|
||||
{
|
||||
Name: "incorrect multi filter",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: "<="},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-9]++", Operator: "nregex"},
|
||||
{Key: v3.AttributeKey{Key: "key", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Expr: `attributes["key"] != nil && attributes["key"] <= 10 and body not matches "[0-9]++" and "key" not in attributes`,
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.key <= 10 and body not regexp '[0-9]++' and attribute.key not exists",
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
}
|
||||
@@ -267,248 +261,234 @@ func TestExpressionVSEntry(t *testing.T) {
|
||||
|
||||
var testCases = []struct {
|
||||
Name string
|
||||
Query *v3.FilterSet
|
||||
Query *qbtypes.Filter
|
||||
ExpectedMatches []int
|
||||
}{
|
||||
{
|
||||
Name: "resource equal (env)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'prod'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 4, 5, 9, 11, 13, 14, 17, 18},
|
||||
},
|
||||
{
|
||||
Name: "resource not equal (env)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "!="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env != 'prod'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 3, 6, 7, 8, 10, 12, 15, 16, 19, 20},
|
||||
},
|
||||
{
|
||||
Name: "attribute less than (numeric)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "count", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}, Value: 8, Operator: "<"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.count < 8",
|
||||
},
|
||||
ExpectedMatches: []int{4},
|
||||
},
|
||||
{
|
||||
Name: "attribute greater than (numeric)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "count", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}, Value: 8, Operator: ">"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.count > 8",
|
||||
},
|
||||
ExpectedMatches: []int{5},
|
||||
},
|
||||
{
|
||||
Name: "body contains (case insensitive)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body contains 'checkbody'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 9, 10, 16},
|
||||
},
|
||||
{
|
||||
Name: "body ncontains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "ncontains"},
|
||||
}},
|
||||
Name: "body NOT CONTAINS",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body NOT CONTAINS 'checkbody'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 17},
|
||||
},
|
||||
{
|
||||
Name: "body.msg (case insensitive)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.msg", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: false}, Value: "checkbody", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.msg contains 'checkbody'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 9, 10, 18},
|
||||
},
|
||||
{
|
||||
Name: "body regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex", Operator: "regex"},
|
||||
}},
|
||||
Name: "body REGEXP",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body REGEXP '[0-1]+regex'",
|
||||
},
|
||||
ExpectedMatches: []int{4},
|
||||
},
|
||||
{
|
||||
Name: "body not regex",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex", Operator: "nregex"},
|
||||
}},
|
||||
Name: "body NOT REGEXP",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body NOT REGEXP '[0-1]+regex'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17},
|
||||
},
|
||||
// body.log.message exists/nexists: expr checks "log.message" in fromJSON(body); nested key
|
||||
// semantics depend on signoz stanza helper. Omitted here to avoid coupling to env shape.
|
||||
{
|
||||
Name: "body top-level key exists (body.msg)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.msg", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.msg exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 2, 3, 4, 5, 9, 10, 11, 12, 18},
|
||||
},
|
||||
{
|
||||
Name: "body top-level key not exists (body.missing)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body.missing", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body.missing not exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 18, 20},
|
||||
},
|
||||
{
|
||||
Name: "attribute exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.service exists",
|
||||
},
|
||||
ExpectedMatches: []int{6, 7, 8, 15},
|
||||
},
|
||||
{
|
||||
Name: "attribute not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.service not exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 2, 3, 4, 5, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20},
|
||||
},
|
||||
{
|
||||
Name: "trace_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id exists",
|
||||
},
|
||||
ExpectedMatches: []int{1, 2, 5, 7, 12, 15, 19},
|
||||
},
|
||||
{
|
||||
Name: "trace_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id not exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 3, 4, 6, 8, 9, 10, 11, 13, 14, 16, 17, 18, 20},
|
||||
},
|
||||
{
|
||||
Name: "span_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id exists",
|
||||
},
|
||||
ExpectedMatches: []int{1, 3, 5, 12, 17, 20},
|
||||
},
|
||||
{
|
||||
Name: "span_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id not exists",
|
||||
},
|
||||
ExpectedMatches: []int{0, 2, 4, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 18, 19},
|
||||
},
|
||||
{
|
||||
Name: "in (attribute in list)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: []any{"info", "error"}, Operator: "in"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level in ['info', 'error']",
|
||||
},
|
||||
ExpectedMatches: []int{0, 1, 2, 14, 16, 20},
|
||||
},
|
||||
{
|
||||
Name: "not in (attribute not in list)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: []any{"error", "warn"}, Operator: "nin"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level not in ['error', 'warn']",
|
||||
},
|
||||
ExpectedMatches: []int{0, 2, 3, 16, 20},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "staging", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "info", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'staging' and attribute.level = 'info'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 16},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND (two attributes)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "auth", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: "nexists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.service = 'auth' and attribute.level not exists",
|
||||
},
|
||||
ExpectedMatches: []int{6, 7},
|
||||
},
|
||||
// Multi-filter variations: body + attribute, three conditions, trace/span + attribute
|
||||
{
|
||||
Name: "multi filter AND body contains + attribute",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "Connection", Operator: "contains"},
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body contains 'Connection' and resource.env = 'prod'",
|
||||
},
|
||||
ExpectedMatches: []int{14},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND body contains + service",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "login", Operator: "contains"},
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "auth", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body contains 'login' and attribute.service = 'auth'",
|
||||
},
|
||||
ExpectedMatches: []int{6, 15},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND env + level (prod error)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "error", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'prod' and attribute.level = 'error'",
|
||||
},
|
||||
ExpectedMatches: []int{1, 14},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND three conditions (staging + checkbody + info)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "staging", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "contains"},
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "info", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'staging' and body contains 'checkbody' and attribute.level = 'info'",
|
||||
},
|
||||
ExpectedMatches: []int{2, 16},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND trace_id exists + body contains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "checkbody", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id exists and body contains 'checkbody'",
|
||||
},
|
||||
ExpectedMatches: []int{2},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND span_id nexists + service auth",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
{Key: v3.AttributeKey{Key: "service", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "auth", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "span_id not exists and attribute.service = 'auth'",
|
||||
},
|
||||
ExpectedMatches: []int{6, 7, 15},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND body regex + attribute",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "[0-1]+regex", Operator: "regex"},
|
||||
{Key: v3.AttributeKey{Key: "code", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "200", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "body regexp '[0-1]+regex' and attribute.code = '200'",
|
||||
},
|
||||
ExpectedMatches: []int{4},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND no trace_id + no span_id + env prod",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "prod", Operator: "="},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "trace_id not exists and span_id not exists and resource.env = 'prod'",
|
||||
},
|
||||
ExpectedMatches: []int{0, 4, 9, 11, 13, 14, 18},
|
||||
},
|
||||
{
|
||||
Name: "multi filter AND level warn + body contains",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "level", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "warn", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "disk", Operator: "contains"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.level = 'warn' and body contains 'disk'",
|
||||
},
|
||||
ExpectedMatches: []int{17},
|
||||
},
|
||||
{
|
||||
Name: "no matches (attribute value not present)",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "env", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "never", Operator: "="},
|
||||
}},
|
||||
Name: "no matches (resource value not present)",
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "resource.env = 'never'",
|
||||
},
|
||||
ExpectedMatches: []int{},
|
||||
},
|
||||
{
|
||||
Name: "attribute equal and trace_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "code", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "404", Operator: "="},
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Query: &qbtypes.Filter{
|
||||
Expression: "attribute.code = '404' and trace_id exists",
|
||||
},
|
||||
ExpectedMatches: []int{5},
|
||||
},
|
||||
}
|
||||
|
||||
392
pkg/querybuilder/filter_introspect.go
Normal file
392
pkg/querybuilder/filter_introspect.go
Normal file
@@ -0,0 +1,392 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
// ExtractFilterExprTree parses a v5 filter expression and returns a logical
|
||||
// tree that preserves full boolean structure (AND/OR, parentheses, NOT) as
|
||||
// well as the individual conditions (keys, operator, values).
|
||||
//
|
||||
// This can be reused by callers that need deeper introspection into the logic
|
||||
// of the filter expression without constructing any SQL or query-engine
|
||||
// specific structures.
|
||||
func ExtractFilterExprTree(expr string) (*qbtypes.FilterExprNode, error) {
|
||||
if strings.TrimSpace(expr) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Setup the ANTLR parsing pipeline (same grammar as PrepareWhereClause).
|
||||
input := antlr.NewInputStream(expr)
|
||||
lexer := grammar.NewFilterQueryLexer(input)
|
||||
|
||||
lexerErrorListener := NewErrorListener()
|
||||
lexer.RemoveErrorListeners()
|
||||
lexer.AddErrorListener(lexerErrorListener)
|
||||
|
||||
tokens := antlr.NewCommonTokenStream(lexer, 0)
|
||||
parserErrorListener := NewErrorListener()
|
||||
parser := grammar.NewFilterQueryParser(tokens)
|
||||
parser.RemoveErrorListeners()
|
||||
parser.AddErrorListener(parserErrorListener)
|
||||
|
||||
tree := parser.Query()
|
||||
|
||||
// Handle syntax errors
|
||||
if len(parserErrorListener.SyntaxErrors) > 0 {
|
||||
combinedErrors := errors.Newf(
|
||||
errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"Found %d syntax errors while parsing the search expression.",
|
||||
len(parserErrorListener.SyntaxErrors),
|
||||
)
|
||||
additionals := make([]string, 0, len(parserErrorListener.SyntaxErrors))
|
||||
for _, err := range parserErrorListener.SyntaxErrors {
|
||||
if err.Error() != "" {
|
||||
additionals = append(additionals, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil, combinedErrors.WithAdditional(additionals...)
|
||||
}
|
||||
|
||||
visitor := &filterTreeVisitor{}
|
||||
rootAny := visitor.Visit(tree)
|
||||
if len(visitor.errors) > 0 {
|
||||
combinedErrors := errors.Newf(
|
||||
errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"Found %d errors while parsing the search expression.",
|
||||
len(visitor.errors),
|
||||
)
|
||||
return nil, combinedErrors.WithAdditional(visitor.errors...)
|
||||
}
|
||||
|
||||
root, _ := rootAny.(*qbtypes.FilterExprNode)
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// filterTreeVisitor builds a FilterExprNode tree from the parse tree.
|
||||
type filterTreeVisitor struct {
|
||||
errors []string
|
||||
}
|
||||
|
||||
// Visit dispatches based on node type.
|
||||
func (v *filterTreeVisitor) Visit(tree antlr.ParseTree) any {
|
||||
if tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch t := tree.(type) {
|
||||
case *grammar.QueryContext:
|
||||
return v.VisitQuery(t)
|
||||
case *grammar.ExpressionContext:
|
||||
return v.VisitExpression(t)
|
||||
case *grammar.OrExpressionContext:
|
||||
return v.VisitOrExpression(t)
|
||||
case *grammar.AndExpressionContext:
|
||||
return v.VisitAndExpression(t)
|
||||
case *grammar.UnaryExpressionContext:
|
||||
return v.VisitUnaryExpression(t)
|
||||
case *grammar.PrimaryContext:
|
||||
return v.VisitPrimary(t)
|
||||
case *grammar.ComparisonContext:
|
||||
return v.VisitComparison(t)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitQuery(ctx *grammar.QueryContext) any {
|
||||
return v.Visit(ctx.Expression())
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitExpression(ctx *grammar.ExpressionContext) any {
|
||||
return v.Visit(ctx.OrExpression())
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
|
||||
andExprs := ctx.AllAndExpression()
|
||||
children := make([]*qbtypes.FilterExprNode, 0, len(andExprs))
|
||||
|
||||
for _, andExpr := range andExprs {
|
||||
if node, ok := v.Visit(andExpr).(*qbtypes.FilterExprNode); ok && node != nil {
|
||||
children = append(children, node)
|
||||
}
|
||||
}
|
||||
|
||||
if len(children) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(children) == 1 {
|
||||
return children[0]
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitAndExpression(ctx *grammar.AndExpressionContext) any {
|
||||
unaryExprs := ctx.AllUnaryExpression()
|
||||
children := make([]*qbtypes.FilterExprNode, 0, len(unaryExprs))
|
||||
|
||||
for _, unary := range unaryExprs {
|
||||
if node, ok := v.Visit(unary).(*qbtypes.FilterExprNode); ok && node != nil {
|
||||
children = append(children, node)
|
||||
}
|
||||
}
|
||||
|
||||
if len(children) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(children) == 1 {
|
||||
return children[0]
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) any {
|
||||
node, _ := v.Visit(ctx.Primary()).(*qbtypes.FilterExprNode)
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ctx.NOT() != nil {
|
||||
node.Negated = !node.Negated
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any {
|
||||
switch {
|
||||
case ctx.OrExpression() != nil:
|
||||
return v.Visit(ctx.OrExpression())
|
||||
case ctx.Comparison() != nil:
|
||||
return v.Visit(ctx.Comparison())
|
||||
default:
|
||||
// We intentionally ignore FullText/FunctionCall here for the tree
|
||||
// representation. They can be added later if needed.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// VisitComparison builds a leaf node with a single ParsedFilterCondition.
|
||||
func (v *filterTreeVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
keys := v.buildKeys(ctx.Key())
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle EXISTS specially
|
||||
if ctx.EXISTS() != nil {
|
||||
op := qbtypes.FilterOperatorExists
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotExists
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{{
|
||||
Keys: keys,
|
||||
Op: op,
|
||||
Value: nil,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IN / NOT IN
|
||||
if ctx.InClause() != nil || ctx.NotInClause() != nil {
|
||||
values := v.buildValuesFromInClause(ctx.InClause(), ctx.NotInClause())
|
||||
|
||||
op := qbtypes.FilterOperatorIn
|
||||
if ctx.NotInClause() != nil {
|
||||
op = qbtypes.FilterOperatorNotIn
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{{
|
||||
Keys: keys,
|
||||
Op: op,
|
||||
Value: values,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle BETWEEN / NOT BETWEEN
|
||||
if ctx.BETWEEN() != nil {
|
||||
valuesCtx := ctx.AllValue()
|
||||
if len(valuesCtx) != 2 {
|
||||
v.errors = append(v.errors, "BETWEEN operator requires exactly two values")
|
||||
return nil
|
||||
}
|
||||
|
||||
value1 := v.buildValue(valuesCtx[0])
|
||||
value2 := v.buildValue(valuesCtx[1])
|
||||
|
||||
op := qbtypes.FilterOperatorBetween
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotBetween
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{{
|
||||
Keys: keys,
|
||||
Op: op,
|
||||
Value: []any{value1, value2},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// All remaining operators have exactly one value.
|
||||
valuesCtx := ctx.AllValue()
|
||||
if len(valuesCtx) == 0 {
|
||||
v.errors = append(v.errors, "comparison operator requires a value")
|
||||
return nil
|
||||
}
|
||||
|
||||
value := v.buildValue(valuesCtx[0])
|
||||
|
||||
var op qbtypes.FilterOperator
|
||||
switch {
|
||||
case ctx.EQUALS() != nil:
|
||||
op = qbtypes.FilterOperatorEqual
|
||||
case ctx.NOT_EQUALS() != nil || ctx.NEQ() != nil:
|
||||
op = qbtypes.FilterOperatorNotEqual
|
||||
case ctx.LT() != nil:
|
||||
op = qbtypes.FilterOperatorLessThan
|
||||
case ctx.LE() != nil:
|
||||
op = qbtypes.FilterOperatorLessThanOrEq
|
||||
case ctx.GT() != nil:
|
||||
op = qbtypes.FilterOperatorGreaterThan
|
||||
case ctx.GE() != nil:
|
||||
op = qbtypes.FilterOperatorGreaterThanOrEq
|
||||
case ctx.LIKE() != nil:
|
||||
op = qbtypes.FilterOperatorLike
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotLike
|
||||
}
|
||||
case ctx.ILIKE() != nil:
|
||||
op = qbtypes.FilterOperatorILike
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotILike
|
||||
}
|
||||
case ctx.REGEXP() != nil:
|
||||
op = qbtypes.FilterOperatorRegexp
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotRegexp
|
||||
}
|
||||
case ctx.CONTAINS() != nil:
|
||||
op = qbtypes.FilterOperatorContains
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotContains
|
||||
}
|
||||
default:
|
||||
v.errors = append(v.errors, fmt.Sprintf("unsupported comparison operator in expression: %s", ctx.GetText()))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{{
|
||||
Keys: keys,
|
||||
Op: op,
|
||||
Value: value,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// buildKeys turns a key context into a slice of TelemetryFieldKey.
|
||||
func (v *filterTreeVisitor) buildKeys(ctx grammar.IKeyContext) []*telemetrytypes.TelemetryFieldKey {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
text := ctx.GetText()
|
||||
key := telemetrytypes.GetFieldKeyFromKeyText(text)
|
||||
return []*telemetrytypes.TelemetryFieldKey{&key}
|
||||
}
|
||||
|
||||
// buildValuesFromInClause handles the IN/NOT IN value side.
|
||||
func (v *filterTreeVisitor) buildValuesFromInClause(in grammar.IInClauseContext, notIn grammar.INotInClauseContext) []any {
|
||||
var ctxVal any
|
||||
if in != nil {
|
||||
ctxVal = v.visitInClause(in)
|
||||
} else if notIn != nil {
|
||||
ctxVal = v.visitNotInClause(notIn)
|
||||
}
|
||||
|
||||
switch ret := ctxVal.(type) {
|
||||
case []any:
|
||||
return ret
|
||||
case any:
|
||||
if ret != nil {
|
||||
return []any{ret}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) visitInClause(ctx grammar.IInClauseContext) any {
|
||||
if ctx.ValueList() != nil {
|
||||
return v.visitValueList(ctx.ValueList())
|
||||
}
|
||||
return v.buildValue(ctx.Value())
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) visitNotInClause(ctx grammar.INotInClauseContext) any {
|
||||
if ctx.ValueList() != nil {
|
||||
return v.visitValueList(ctx.ValueList())
|
||||
}
|
||||
return v.buildValue(ctx.Value())
|
||||
}
|
||||
|
||||
func (v *filterTreeVisitor) visitValueList(ctx grammar.IValueListContext) any {
|
||||
values := ctx.AllValue()
|
||||
parts := make([]any, 0, len(values))
|
||||
for _, val := range values {
|
||||
parts = append(parts, v.buildValue(val))
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
// buildValue converts literal values into Go types (string, float64, bool).
|
||||
func (v *filterTreeVisitor) buildValue(ctx grammar.IValueContext) any {
|
||||
switch {
|
||||
case ctx == nil:
|
||||
return nil
|
||||
case ctx.QUOTED_TEXT() != nil:
|
||||
txt := ctx.QUOTED_TEXT().GetText()
|
||||
return trimQuotes(txt)
|
||||
case ctx.NUMBER() != nil:
|
||||
number, err := strconv.ParseFloat(ctx.NUMBER().GetText(), 64)
|
||||
if err != nil {
|
||||
v.errors = append(v.errors, fmt.Sprintf("failed to parse number %s", ctx.NUMBER().GetText()))
|
||||
return nil
|
||||
}
|
||||
return number
|
||||
case ctx.BOOL() != nil:
|
||||
boolText := strings.ToLower(ctx.BOOL().GetText())
|
||||
return boolText == "true"
|
||||
case ctx.KEY() != nil:
|
||||
return ctx.KEY().GetText()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
351
pkg/querybuilder/filter_introspect_test.go
Normal file
351
pkg/querybuilder/filter_introspect_test.go
Normal file
@@ -0,0 +1,351 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// helper to build a simple key with unspecified context/datatype.
|
||||
func fk(_ *testing.T, name string) *telemetrytypes.TelemetryFieldKey {
|
||||
key := telemetrytypes.GetFieldKeyFromKeyText(name)
|
||||
return &key
|
||||
}
|
||||
|
||||
func TestExtractFilterExprTree_NestedConditions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expr string
|
||||
want *qbtypes.FilterExprNode
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty expression returns nil",
|
||||
expr: " ",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid expression returns error",
|
||||
expr: "attributes.key =",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "single simple equality leaf",
|
||||
expr: "attributes.host.name = 'frontend'",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.host.name")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "frontend",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AND with BETWEEN numeric range and NOT boolean comparison",
|
||||
expr: "attributes.status_code BETWEEN 500 AND 599 AND NOT attributes.is_error = true",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.status_code")},
|
||||
Op: qbtypes.FilterOperatorBetween,
|
||||
Value: []any{
|
||||
float64(500),
|
||||
float64(599),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Negated: true,
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.is_error")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OR with IN list and NOT IN list using different syntaxes",
|
||||
expr: "attributes.service.name IN ('api','worker') OR resource.region NOT IN [\"us-east-1\",\"us-west-2\"]",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.service.name")},
|
||||
Op: qbtypes.FilterOperatorIn,
|
||||
Value: []any{
|
||||
"api",
|
||||
"worker",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "resource.region")},
|
||||
Op: qbtypes.FilterOperatorNotIn,
|
||||
Value: []any{
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AND chain of string pattern operators with NOT variants",
|
||||
expr: "attributes.message LIKE 'error%' AND attributes.message NOT ILIKE '%debug%' AND attributes.message REGEXP 'err[0-9]+' AND attributes.message NOT CONTAINS 'trace'",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.message")},
|
||||
Op: qbtypes.FilterOperatorLike,
|
||||
Value: "error%",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.message")},
|
||||
Op: qbtypes.FilterOperatorNotILike,
|
||||
Value: "%debug%",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.message")},
|
||||
Op: qbtypes.FilterOperatorRegexp,
|
||||
Value: "err[0-9]+",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.message")},
|
||||
Op: qbtypes.FilterOperatorNotContains,
|
||||
Value: "trace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EXISTS and NOT EXISTS in OR expression",
|
||||
expr: "attributes.host EXISTS OR attributes.cluster NOT EXISTS",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.host")},
|
||||
Op: qbtypes.FilterOperatorExists,
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.cluster")},
|
||||
Op: qbtypes.FilterOperatorNotExists,
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "KEY used as value in equality",
|
||||
expr: "attributes.left = other_key",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.left")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "other_key",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested OR inside AND with NOT on inner group",
|
||||
// NOT applies to the whole parenthesized OR group.
|
||||
expr: "attributes.env = 'prod' AND NOT (attributes.team = 'core' OR attributes.team = 'platform')",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.env")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Negated: true,
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.team")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "core",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.team")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "platform",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple nesting levels mixing AND/OR",
|
||||
expr: "(attributes.status = 'critical' OR attributes.status = 'warning') AND (resource.region = 'us-east-1' OR (resource.region = 'us-west-2' AND attributes.tier = 'backend'))",
|
||||
want: &qbtypes.FilterExprNode{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.status")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.status")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "warning",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpOr,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "resource.region")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "us-east-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpAnd,
|
||||
Children: []*qbtypes.FilterExprNode{
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "resource.region")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Op: qbtypes.LogicalOpLeaf,
|
||||
Conditions: []qbtypes.FilterCondition{
|
||||
{
|
||||
Keys: []*telemetrytypes.TelemetryFieldKey{fk(t, "attributes.tier")},
|
||||
Op: qbtypes.FilterOperatorEqual,
|
||||
Value: "backend",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ExtractFilterExprTree(tt.expr)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("unexpected tree for expr %q\n got: %#v\n want: %#v", tt.expr, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields/implfields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
@@ -30,7 +28,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
@@ -47,19 +44,9 @@ type Handlers struct {
|
||||
FlaggerHandler flagger.Handler
|
||||
GatewayHandler gateway.Handler
|
||||
Role role.Handler
|
||||
Fields fields.Handler
|
||||
}
|
||||
|
||||
func NewHandlers(
|
||||
modules Modules,
|
||||
providerSettings factory.ProviderSettings,
|
||||
querier querier.Querier,
|
||||
licensing licensing.Licensing,
|
||||
global global.Global,
|
||||
flaggerService flagger.Flagger,
|
||||
gatewayService gateway.Gateway,
|
||||
telemetryMetadataStore telemetrytypes.MetadataStore,
|
||||
) Handlers {
|
||||
func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing, global global.Global, flaggerService flagger.Flagger, gatewayService gateway.Gateway) Handlers {
|
||||
return Handlers{
|
||||
SavedView: implsavedview.NewHandler(modules.SavedView),
|
||||
Apdex: implapdex.NewHandler(modules.Apdex),
|
||||
@@ -74,6 +61,5 @@ func NewHandlers(
|
||||
FlaggerHandler: flagger.NewHandler(flaggerService),
|
||||
GatewayHandler: gateway.NewHandler(gatewayService),
|
||||
Role: implrole.NewHandler(modules.RoleSetter, modules.RoleGetter),
|
||||
Fields: implfields.NewHandler(providerSettings, telemetryMetadataStore),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
grantModule := implrole.NewGranter(implrole.NewStore(sqlstore), nil)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, roleSetter, roleGetter, grantModule)
|
||||
|
||||
handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil, nil, nil)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil, nil)
|
||||
|
||||
reflectVal := reflect.ValueOf(handlers)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
|
||||
@@ -2,7 +2,6 @@ package signoz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
@@ -16,7 +15,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
@@ -25,8 +23,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
"github.com/swaggest/openapi-go"
|
||||
"github.com/swaggest/openapi-go/openapi3"
|
||||
@@ -56,16 +52,11 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ gateway.Handler }{},
|
||||
struct{ role.Getter }{},
|
||||
struct{ role.Handler }{},
|
||||
struct{ fields.Handler }{},
|
||||
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Register routes that live outside the APIServer modules
|
||||
// so they are discovered by the OpenAPI walker.
|
||||
registerQueryRoutes(apiserver.Router())
|
||||
|
||||
reflector := openapi3.NewReflector()
|
||||
reflector.JSONSchemaReflector().DefaultOptions = append(reflector.JSONSchemaReflector().DefaultOptions, jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
|
||||
if defaultDefName == "RenderSuccessResponse" {
|
||||
@@ -106,25 +97,3 @@ func (openapi *OpenAPI) CreateAndWrite(path string) error {
|
||||
|
||||
return os.WriteFile(path, spec, 0o600)
|
||||
}
|
||||
|
||||
func registerQueryRoutes(router *mux.Router) {
|
||||
router.Handle("/api/v5/query_range", handler.New(
|
||||
func(http.ResponseWriter, *http.Request) {},
|
||||
handler.OpenAPIDef{
|
||||
ID: "QueryRangeV5",
|
||||
Tags: []string{"query"},
|
||||
Summary: "Query range",
|
||||
Description: "Execute a composite query over a time range. Supports builder queries (traces, logs, metrics), formulas, trace operators, PromQL, and ClickHouse SQL.",
|
||||
Request: new(qbtypes.QueryRangeRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(qbtypes.QueryRangeResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{
|
||||
{Name: ctxtypes.AuthTypeAPIKey.StringValue(), Scopes: []string{"VIEWER"}},
|
||||
{Name: ctxtypes.AuthTypeTokenizer.StringValue(), Scopes: []string{"VIEWER"}},
|
||||
},
|
||||
},
|
||||
)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
@@ -249,7 +249,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.GatewayHandler,
|
||||
modules.RoleGetter,
|
||||
handlers.Role,
|
||||
handlers.Fields,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ func New(
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, roleSetter, roleGetter, granter)
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway, telemetryMetadataStore)
|
||||
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway)
|
||||
|
||||
// Initialize the API server
|
||||
apiserver, err := factory.NewProviderFromNamedMap(
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/queryBuilderToExpr"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ type StoreablePipeline struct {
|
||||
|
||||
type GettablePipeline struct {
|
||||
StoreablePipeline
|
||||
Filter *v3.FilterSet `json:"filter"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Config []PipelineOperator `json:"config"`
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func (i *GettablePipeline) ParseRawConfig() error {
|
||||
}
|
||||
|
||||
func (i *GettablePipeline) ParseFilter() error {
|
||||
f := v3.FilterSet{}
|
||||
f := qbtypes.Filter{}
|
||||
err := json.Unmarshal([]byte(i.FilterString), &f)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to parse filter")
|
||||
@@ -200,7 +200,7 @@ type PostablePipeline struct {
|
||||
Alias string `json:"alias"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Filter *v3.FilterSet `json:"filter"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Config []PipelineOperator `json:"config"`
|
||||
}
|
||||
|
||||
@@ -218,6 +218,14 @@ func (p *PostablePipeline) IsValid() error {
|
||||
}
|
||||
|
||||
// check the filter
|
||||
if p.Filter == nil || strings.TrimSpace(p.Filter.Expression) == "" {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "filter.expression is required")
|
||||
}
|
||||
|
||||
// Validate that every field in the expression has an explicit context
|
||||
// (attribute., resource., body., etc) so later pipeline processing does not
|
||||
// need to guess. We intentionally do not validate that the field actually
|
||||
// exists – only that the context is specified.
|
||||
_, err := queryBuilderToExpr.Parse(p.Filter)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,24 +3,13 @@ package pipelinetypes
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsValidPostablePipeline(t *testing.T) {
|
||||
validPipelineFilterSet := &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
validPipelineFilter := &qbtypes.Filter{
|
||||
Expression: `attributes.method = "GET"`,
|
||||
}
|
||||
|
||||
var correctQueriesTest = []struct {
|
||||
@@ -34,7 +23,7 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: validPipelineFilterSet,
|
||||
Filter: validPipelineFilter,
|
||||
Config: []PipelineOperator{},
|
||||
},
|
||||
IsValid: false,
|
||||
@@ -46,7 +35,7 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: validPipelineFilterSet,
|
||||
Filter: validPipelineFilter,
|
||||
Config: []PipelineOperator{},
|
||||
},
|
||||
IsValid: false,
|
||||
@@ -58,7 +47,7 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: validPipelineFilterSet,
|
||||
Filter: validPipelineFilter,
|
||||
Config: []PipelineOperator{},
|
||||
},
|
||||
IsValid: true,
|
||||
@@ -70,19 +59,21 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeUnspecified,
|
||||
},
|
||||
Operator: "regex",
|
||||
Value: "[0-9A-Z*",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "",
|
||||
},
|
||||
},
|
||||
IsValid: false,
|
||||
},
|
||||
{
|
||||
Name: "Filter without context prefix on field",
|
||||
Pipeline: PostablePipeline{
|
||||
OrderID: 1,
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: `method = "GET"`,
|
||||
},
|
||||
},
|
||||
IsValid: false,
|
||||
@@ -94,19 +85,19 @@ func TestIsValidPostablePipeline(t *testing.T) {
|
||||
Name: "pipeline 1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: validPipelineFilterSet,
|
||||
Filter: validPipelineFilter,
|
||||
},
|
||||
IsValid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range correctQueriesTest {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
err := test.Pipeline.IsValid()
|
||||
if test.IsValid {
|
||||
So(err, ShouldBeNil)
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
So(err, ShouldBeError)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -365,12 +356,12 @@ var operatorTest = []struct {
|
||||
|
||||
func TestValidOperator(t *testing.T) {
|
||||
for _, test := range operatorTest {
|
||||
Convey(test.Name, t, func() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
err := isValidOperator(test.Operator)
|
||||
if test.IsValid {
|
||||
So(err, ShouldBeNil)
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
So(err, ShouldBeError)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
|
||||
// LogicalOp represents how child expressions are combined.
|
||||
type LogicalOp string
|
||||
|
||||
const (
|
||||
// LogicalOpLeaf represents a leaf node containing one or more simple conditions.
|
||||
LogicalOpLeaf LogicalOp = "LEAF"
|
||||
// LogicalOpAnd represents an AND combination of children.
|
||||
LogicalOpAnd LogicalOp = "AND"
|
||||
// LogicalOpOr represents an OR combination of children.
|
||||
LogicalOpOr LogicalOp = "OR"
|
||||
)
|
||||
|
||||
// FilterExprNode is a reusable logical representation of a filter expression.
|
||||
//
|
||||
// - Leaf nodes (Op == LogicalOpLeaf) contain one or more ParsedFilterCondition.
|
||||
// - Non-leaf nodes (Op == LogicalOpAnd/LogicalOpOr) contain Children.
|
||||
// - Negated indicates a leading NOT applied to this subtree.
|
||||
type FilterExprNode struct {
|
||||
Op LogicalOp
|
||||
Negated bool
|
||||
Conditions []FilterCondition
|
||||
Children []*FilterExprNode
|
||||
}
|
||||
|
||||
func (f *FilterExprNode) Flatten() []FilterCondition {
|
||||
var conditions []FilterCondition
|
||||
var walk func(node *FilterExprNode)
|
||||
|
||||
walk = func(node *FilterExprNode) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if node.Op == LogicalOpLeaf {
|
||||
conditions = append(conditions, node.Conditions...)
|
||||
}
|
||||
for _, child := range node.Children {
|
||||
walk(child)
|
||||
}
|
||||
}
|
||||
|
||||
walk(f)
|
||||
return conditions
|
||||
}
|
||||
|
||||
// FilterCondition represents a single comparison or existence check
|
||||
// extracted from a filter expression.
|
||||
//
|
||||
// - Keys: one or more logical field keys on the left-hand side (see where_clause_visitor.VisitKey
|
||||
// for why one expression key can resolve to multiple TelemetryFieldKeys).
|
||||
// - Op: filter operator (e.g. =, !=, in, exists, between).
|
||||
// - Value: right-hand side literal (any type: single value, slice for IN/NOT IN, nil for EXISTS, etc.).
|
||||
type FilterCondition struct {
|
||||
Keys []*telemetrytypes.TelemetryFieldKey
|
||||
Op FilterOperator
|
||||
Value any
|
||||
}
|
||||
@@ -1,707 +0,0 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
// Enum returns the acceptable values for QueryType.
|
||||
func (QueryType) Enum() []any {
|
||||
return []any{
|
||||
QueryTypeBuilder,
|
||||
QueryTypeFormula,
|
||||
// Not yet supported.
|
||||
// QueryTypeSubQuery,
|
||||
// QueryTypeJoin,
|
||||
QueryTypeTraceOperator,
|
||||
QueryTypeClickHouseSQL,
|
||||
QueryTypePromQL,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for RequestType.
|
||||
func (RequestType) Enum() []any {
|
||||
return []any{
|
||||
RequestTypeScalar,
|
||||
RequestTypeTimeSeries,
|
||||
RequestTypeRaw,
|
||||
RequestTypeRawStream,
|
||||
RequestTypeTrace,
|
||||
// RequestTypeDistribution,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for FunctionName.
|
||||
func (FunctionName) Enum() []any {
|
||||
return []any{
|
||||
FunctionNameCutOffMin,
|
||||
FunctionNameCutOffMax,
|
||||
FunctionNameClampMin,
|
||||
FunctionNameClampMax,
|
||||
FunctionNameAbsolute,
|
||||
FunctionNameRunningDiff,
|
||||
FunctionNameLog2,
|
||||
FunctionNameLog10,
|
||||
FunctionNameCumulativeSum,
|
||||
FunctionNameEWMA3,
|
||||
FunctionNameEWMA5,
|
||||
FunctionNameEWMA7,
|
||||
FunctionNameMedian3,
|
||||
FunctionNameMedian5,
|
||||
FunctionNameMedian7,
|
||||
FunctionNameTimeShift,
|
||||
FunctionNameAnomaly,
|
||||
FunctionNameFillZero,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for OrderDirection.
|
||||
func (OrderDirection) Enum() []any {
|
||||
return []any{
|
||||
OrderDirectionAsc,
|
||||
OrderDirectionDesc,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for ReduceTo.
|
||||
func (ReduceTo) Enum() []any {
|
||||
return []any{
|
||||
ReduceToSum,
|
||||
ReduceToCount,
|
||||
ReduceToAvg,
|
||||
ReduceToMin,
|
||||
ReduceToMax,
|
||||
ReduceToLast,
|
||||
ReduceToMedian,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for VariableType.
|
||||
func (VariableType) Enum() []any {
|
||||
return []any{
|
||||
QueryVariableType,
|
||||
DynamicVariableType,
|
||||
CustomVariableType,
|
||||
TextBoxVariableType,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for JoinType.
|
||||
func (JoinType) Enum() []any {
|
||||
return []any{
|
||||
JoinTypeInner,
|
||||
JoinTypeLeft,
|
||||
JoinTypeRight,
|
||||
JoinTypeFull,
|
||||
JoinTypeCross,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for ColumnType.
|
||||
func (ColumnType) Enum() []any {
|
||||
return []any{
|
||||
ColumnTypeGroup,
|
||||
ColumnTypeAggregation,
|
||||
}
|
||||
}
|
||||
|
||||
// queryEnvelopeBuilderTrace is the OpenAPI schema for a QueryEnvelope with type=builder_query and signal=traces.
|
||||
type queryEnvelopeBuilderTrace struct {
|
||||
Type QueryType `json:"type" description:"The type of the query."`
|
||||
Spec QueryBuilderQuery[TraceAggregation] `json:"spec" description:"The trace builder query specification."`
|
||||
}
|
||||
|
||||
// queryEnvelopeBuilderLog is the OpenAPI schema for a QueryEnvelope with type=builder_query and signal=logs.
|
||||
type queryEnvelopeBuilderLog struct {
|
||||
Type QueryType `json:"type" description:"The type of the query."`
|
||||
Spec QueryBuilderQuery[LogAggregation] `json:"spec" description:"The log builder query specification."`
|
||||
}
|
||||
|
||||
// queryEnvelopeBuilderMetric is the OpenAPI schema for a QueryEnvelope with type=builder_query and signal=metrics.
|
||||
type queryEnvelopeBuilderMetric struct {
|
||||
Type QueryType `json:"type" description:"The type of the query."`
|
||||
Spec QueryBuilderQuery[MetricAggregation] `json:"spec" description:"The metric builder query specification."`
|
||||
}
|
||||
|
||||
// queryEnvelopeFormula is the OpenAPI schema for a QueryEnvelope with type=builder_formula.
|
||||
type queryEnvelopeFormula struct {
|
||||
Type QueryType `json:"type" description:"The type of the query."`
|
||||
Spec QueryBuilderFormula `json:"spec" description:"The formula specification."`
|
||||
}
|
||||
|
||||
// queryEnvelopeJoin is the OpenAPI schema for a QueryEnvelope with type=builder_join.
|
||||
// type queryEnvelopeJoin struct {
|
||||
// Type QueryType `json:"type" description:"The type of the query."`
|
||||
// Spec QueryBuilderJoin `json:"spec" description:"The join specification."`
|
||||
// }
|
||||
|
||||
// queryEnvelopeTraceOperator is the OpenAPI schema for a QueryEnvelope with type=builder_trace_operator.
|
||||
type queryEnvelopeTraceOperator struct {
|
||||
Type QueryType `json:"type" description:"The type of the query."`
|
||||
Spec QueryBuilderTraceOperator `json:"spec" description:"The trace operator specification."`
|
||||
}
|
||||
|
||||
// queryEnvelopePromQL is the OpenAPI schema for a QueryEnvelope with type=promql.
|
||||
type queryEnvelopePromQL struct {
|
||||
Type QueryType `json:"type" description:"The type of the query."`
|
||||
Spec PromQuery `json:"spec" description:"The PromQL query specification."`
|
||||
}
|
||||
|
||||
// queryEnvelopeClickHouseSQL is the OpenAPI schema for a QueryEnvelope with type=clickhouse_sql.
|
||||
type queryEnvelopeClickHouseSQL struct {
|
||||
Type QueryType `json:"type" description:"The type of the query."`
|
||||
Spec ClickHouseQuery `json:"spec" description:"The ClickHouse SQL query specification."`
|
||||
}
|
||||
|
||||
var _ jsonschema.OneOfExposer = QueryEnvelope{}
|
||||
|
||||
// JSONSchemaOneOf returns the oneOf variants for the QueryEnvelope discriminated union.
|
||||
// Each variant represents a different query type with its corresponding spec schema.
|
||||
func (QueryEnvelope) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
queryEnvelopeBuilderTrace{},
|
||||
queryEnvelopeBuilderLog{},
|
||||
queryEnvelopeBuilderMetric{},
|
||||
queryEnvelopeFormula{},
|
||||
// queryEnvelopeJoin{},
|
||||
queryEnvelopeTraceOperator{},
|
||||
queryEnvelopePromQL{},
|
||||
queryEnvelopeClickHouseSQL{},
|
||||
}
|
||||
}
|
||||
|
||||
var _ jsonschema.Exposer = Step{}
|
||||
|
||||
// JSONSchema returns a custom schema for Step that accepts either a duration string or a number (seconds).
|
||||
func (Step) JSONSchema() (jsonschema.Schema, error) {
|
||||
s := jsonschema.Schema{}
|
||||
s.WithDescription("Step interval. Accepts a Go duration string (e.g., \"60s\", \"1m\", \"1h\") or a number representing seconds (e.g., 60).")
|
||||
|
||||
strSchema := jsonschema.Schema{}
|
||||
strSchema.WithType(jsonschema.String.Type())
|
||||
strSchema.WithExamples("60s", "5m", "1h")
|
||||
strSchema.WithDescription("Duration string (e.g., \"60s\", \"5m\", \"1h\").")
|
||||
|
||||
numSchema := jsonschema.Schema{}
|
||||
numSchema.WithType(jsonschema.Number.Type())
|
||||
numSchema.WithExamples(60, 300, 3600)
|
||||
numSchema.WithDescription("Duration in seconds.")
|
||||
|
||||
s.OneOf = []jsonschema.SchemaOrBool{
|
||||
strSchema.ToSchemaOrBool(),
|
||||
numSchema.ToSchemaOrBool(),
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var _ jsonschema.OneOfExposer = QueryData{}
|
||||
|
||||
// JSONSchemaOneOf documents the polymorphic result types in QueryData.Results.
|
||||
func (QueryData) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
TimeSeriesData{},
|
||||
ScalarData{},
|
||||
RawData{},
|
||||
}
|
||||
}
|
||||
|
||||
var _ jsonschema.Preparer = &QueryRangeRequest{}
|
||||
|
||||
// PrepareJSONSchema adds examples and description to the QueryRangeRequest schema.
|
||||
func (q *QueryRangeRequest) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
schema.WithDescription("Request body for the v5 query range endpoint. Supports builder queries (traces, logs, metrics), formulas, joins, trace operators, PromQL, and ClickHouse SQL queries.")
|
||||
schema.WithExamples(
|
||||
// 1. time_series + traces builder: count spans grouped by service, ordered by count
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"expression": "count()",
|
||||
"alias": "span_count",
|
||||
},
|
||||
},
|
||||
"stepInterval": "60s",
|
||||
"filter": map[string]any{
|
||||
"expression": "service.name = 'frontend'",
|
||||
},
|
||||
"groupBy": []any{
|
||||
map[string]any{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
},
|
||||
"order": []any{
|
||||
map[string]any{
|
||||
"key": map[string]any{"name": "span_count"},
|
||||
"direction": "desc",
|
||||
},
|
||||
},
|
||||
"limit": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 2. time_series + logs builder: count logs grouped by service
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"expression": "count()",
|
||||
"alias": "log_count",
|
||||
},
|
||||
},
|
||||
"stepInterval": "60s",
|
||||
"filter": map[string]any{
|
||||
"expression": "severity_text = 'ERROR'",
|
||||
},
|
||||
"groupBy": []any{
|
||||
map[string]any{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
},
|
||||
"order": []any{
|
||||
map[string]any{
|
||||
"key": map[string]any{"name": "log_count"},
|
||||
"direction": "desc",
|
||||
},
|
||||
},
|
||||
"limit": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 3. time_series + metrics builder (Gauge): latest value averaged across series
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"metricName": "system.cpu.utilization",
|
||||
"timeAggregation": "latest",
|
||||
"spaceAggregation": "avg",
|
||||
},
|
||||
},
|
||||
"stepInterval": "60s",
|
||||
"groupBy": []any{
|
||||
map[string]any{
|
||||
"name": "host.name",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 4. time_series + metrics builder (Sum): rate of cumulative counter
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"metricName": "http.server.duration.count",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
},
|
||||
},
|
||||
"stepInterval": 120,
|
||||
"groupBy": []any{
|
||||
map[string]any{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 5. time_series + metrics builder (Histogram): p99 latency
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"metricName": "http.server.duration.bucket",
|
||||
"spaceAggregation": "p99",
|
||||
},
|
||||
},
|
||||
"stepInterval": "60s",
|
||||
"groupBy": []any{
|
||||
map[string]any{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 6. raw + logs builder: fetch raw log records
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "raw",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"filter": map[string]any{
|
||||
"expression": "severity_text = 'ERROR'",
|
||||
},
|
||||
"selectFields": []any{
|
||||
map[string]any{
|
||||
"name": "body",
|
||||
"fieldContext": "log",
|
||||
},
|
||||
map[string]any{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
},
|
||||
"order": []any{
|
||||
map[string]any{
|
||||
"key": map[string]any{"name": "timestamp", "fieldContext": "log"},
|
||||
"direction": "desc",
|
||||
},
|
||||
map[string]any{
|
||||
"key": map[string]any{"name": "id"},
|
||||
"direction": "desc",
|
||||
},
|
||||
},
|
||||
"limit": 50,
|
||||
"offset": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 7. raw + traces builder: fetch raw span records
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "raw",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"filter": map[string]any{
|
||||
"expression": "service.name = 'frontend' AND has_error = true",
|
||||
},
|
||||
"selectFields": []any{
|
||||
map[string]any{
|
||||
"name": "name",
|
||||
"fieldContext": "span",
|
||||
},
|
||||
map[string]any{
|
||||
"name": "duration_nano",
|
||||
"fieldContext": "span",
|
||||
},
|
||||
},
|
||||
"order": []any{
|
||||
map[string]any{
|
||||
"key": map[string]any{"name": "timestamp", "fieldContext": "span"},
|
||||
"direction": "desc",
|
||||
},
|
||||
},
|
||||
"limit": 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 8. scalar + traces builder: total span count as a single value
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "scalar",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"expression": "count()",
|
||||
"alias": "span_count",
|
||||
},
|
||||
},
|
||||
"filter": map[string]any{
|
||||
"expression": "service.name = 'frontend'",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 9. scalar + logs builder: total error log count as a single value
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "scalar",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"expression": "count()",
|
||||
"alias": "error_count",
|
||||
},
|
||||
},
|
||||
"filter": map[string]any{
|
||||
"expression": "severity_text = 'ERROR'",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 10. scalar + metrics builder: single reduced value with reduceTo
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "scalar",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"metricName": "http.server.duration.count",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "sum",
|
||||
},
|
||||
},
|
||||
"stepInterval": "60s",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 11. builder formula: error rate from two trace queries
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"expression": "countIf(has_error = true)",
|
||||
},
|
||||
},
|
||||
"stepInterval": "60s",
|
||||
"groupBy": []any{
|
||||
map[string]any{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "B",
|
||||
"signal": "traces",
|
||||
"aggregations": []any{
|
||||
map[string]any{
|
||||
"expression": "count()",
|
||||
},
|
||||
},
|
||||
"stepInterval": "60s",
|
||||
"groupBy": []any{
|
||||
map[string]any{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_formula",
|
||||
"spec": map[string]any{
|
||||
"name": "error_rate",
|
||||
"expression": "A / B * 100",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 12. PromQL query with UTF-8 dot metric name
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "promql",
|
||||
"spec": map[string]any{
|
||||
"name": "request_rate",
|
||||
"query": "sum(rate({\"http.server.duration.count\"}[5m])) by (\"service.name\")",
|
||||
"step": 60,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 13. ClickHouse SQL — time_series
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": map[string]any{
|
||||
"name": "span_rate",
|
||||
"query": "SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, count() AS value FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= $start_datetime AND timestamp <= $end_datetime GROUP BY ts ORDER BY ts",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 14. ClickHouse SQL — raw
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "raw",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": map[string]any{
|
||||
"name": "recent_errors",
|
||||
"query": "SELECT timestamp, body FROM signoz_logs.distributed_logs_v2 WHERE timestamp >= $start_timestamp_nano AND timestamp <= $end_timestamp_nano AND severity_text = 'ERROR' ORDER BY timestamp DESC LIMIT 100",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 15. ClickHouse SQL — scalar
|
||||
map[string]any{
|
||||
"schemaVersion": "v1",
|
||||
"start": 1640995200000,
|
||||
"end": 1640998800000,
|
||||
"requestType": "scalar",
|
||||
"compositeQuery": map[string]any{
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": map[string]any{
|
||||
"name": "total_spans",
|
||||
"query": "SELECT count() AS value FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= $start_datetime AND timestamp <= $end_datetime",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ jsonschema.Preparer = &QueryRangeResponse{}
|
||||
|
||||
// PrepareJSONSchema adds description to the QueryRangeResponse schema.
|
||||
func (q *QueryRangeResponse) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
schema.WithDescription("Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series, ScalarData for scalar, or RawData for raw requests.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ jsonschema.Preparer = &CompositeQuery{}
|
||||
|
||||
// PrepareJSONSchema adds description to the CompositeQuery schema.
|
||||
func (c *CompositeQuery) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
schema.WithDescription("Composite query containing one or more query envelopes. Each query envelope specifies its type and corresponding spec.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ jsonschema.Preparer = &ExecStats{}
|
||||
|
||||
// PrepareJSONSchema adds description to the ExecStats schema.
|
||||
func (e *ExecStats) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
schema.WithDescription("Execution statistics for the query, including rows scanned, bytes scanned, and duration.")
|
||||
return nil
|
||||
}
|
||||
@@ -266,110 +266,3 @@ type FieldValueSelector struct {
|
||||
Value string `json:"value"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type GettableFieldKeys struct {
|
||||
Keys map[string][]*TelemetryFieldKey `json:"keys"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
type PostableFieldKeysParams struct {
|
||||
Signal Signal `query:"signal"`
|
||||
Source Source `query:"source"`
|
||||
Limit int `query:"limit"`
|
||||
StartUnixMilli int64 `query:"startUnixMilli"`
|
||||
EndUnixMilli int64 `query:"endUnixMilli"`
|
||||
FieldContext FieldContext `query:"fieldContext"`
|
||||
FieldDataType FieldDataType `query:"fieldDataType"`
|
||||
MetricName string `query:"metricName"`
|
||||
SearchText string `query:"searchText"`
|
||||
}
|
||||
|
||||
type GettableFieldValues struct {
|
||||
Values *TelemetryFieldValues `json:"values"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
type PostableFieldValueParams struct {
|
||||
PostableFieldKeysParams
|
||||
Name string `query:"name"`
|
||||
ExistingQuery string `query:"existingQuery"`
|
||||
}
|
||||
|
||||
func NewFieldKeySelectorFromPostableFieldKeysParams(params PostableFieldKeysParams) *FieldKeySelector {
|
||||
var req FieldKeySelector
|
||||
|
||||
if params.StartUnixMilli != 0 {
|
||||
req.StartUnixMilli = params.StartUnixMilli
|
||||
// Round down to the nearest 6 hours (21600000 milliseconds)
|
||||
req.StartUnixMilli -= req.StartUnixMilli % 21600000
|
||||
}
|
||||
|
||||
if params.EndUnixMilli != 0 {
|
||||
req.EndUnixMilli = params.EndUnixMilli
|
||||
}
|
||||
|
||||
req.Signal = params.Signal
|
||||
req.Source = params.Source
|
||||
req.FieldContext = params.FieldContext
|
||||
req.FieldDataType = params.FieldDataType
|
||||
req.SelectorMatchType = FieldSelectorMatchTypeFuzzy
|
||||
|
||||
if params.Limit != 0 {
|
||||
req.Limit = params.Limit
|
||||
} else {
|
||||
req.Limit = 1000
|
||||
}
|
||||
|
||||
if params.MetricName != "" {
|
||||
req.MetricContext = &MetricContext{
|
||||
MetricName: params.MetricName,
|
||||
}
|
||||
}
|
||||
|
||||
req.Name = params.SearchText
|
||||
if params.SearchText != "" && params.FieldContext == FieldContextUnspecified {
|
||||
parsedFieldKey := GetFieldKeyFromKeyText(params.SearchText)
|
||||
if parsedFieldKey.FieldContext != FieldContextUnspecified {
|
||||
// Only apply inferred context if it is valid for the current signal
|
||||
if isContextValidForSignal(parsedFieldKey.FieldContext, req.Signal) {
|
||||
req.Name = parsedFieldKey.Name
|
||||
req.FieldContext = parsedFieldKey.FieldContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &req
|
||||
}
|
||||
|
||||
func NewFieldValueSelectorFromPostableFieldValueParams(params PostableFieldValueParams) *FieldValueSelector {
|
||||
|
||||
keySelector := NewFieldKeySelectorFromPostableFieldKeysParams(params.PostableFieldKeysParams)
|
||||
|
||||
fieldValueSelector := &FieldValueSelector{
|
||||
FieldKeySelector: keySelector,
|
||||
}
|
||||
|
||||
fieldValueSelector.Name = params.Name
|
||||
|
||||
if params.Name != "" && fieldValueSelector.FieldContext == FieldContextUnspecified {
|
||||
parsedFieldKey := GetFieldKeyFromKeyText(params.Name)
|
||||
if parsedFieldKey.FieldContext != FieldContextUnspecified {
|
||||
// Only apply inferred context if it is valid for the current signal
|
||||
if isContextValidForSignal(parsedFieldKey.FieldContext, fieldValueSelector.Signal) {
|
||||
fieldValueSelector.Name = parsedFieldKey.Name
|
||||
fieldValueSelector.FieldContext = parsedFieldKey.FieldContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldValueSelector.ExistingQuery = params.ExistingQuery
|
||||
fieldValueSelector.Value = params.SearchText
|
||||
|
||||
if params.Limit != 0 {
|
||||
fieldValueSelector.Limit = params.Limit
|
||||
} else {
|
||||
fieldValueSelector.Limit = 50
|
||||
}
|
||||
|
||||
return fieldValueSelector
|
||||
}
|
||||
|
||||
@@ -154,21 +154,3 @@ func (f FieldContext) TagType() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isContextValidForSignal(ctx FieldContext, signal Signal) bool {
|
||||
if ctx == FieldContextResource ||
|
||||
ctx == FieldContextAttribute ||
|
||||
ctx == FieldContextScope {
|
||||
return true
|
||||
}
|
||||
|
||||
switch signal.StringValue() {
|
||||
case SignalLogs.StringValue():
|
||||
return ctx == FieldContextLog || ctx == FieldContextBody
|
||||
case SignalTraces.StringValue():
|
||||
return ctx == FieldContextSpan || ctx == FieldContextEvent || ctx == FieldContextTrace
|
||||
case SignalMetrics.StringValue():
|
||||
return ctx == FieldContextMetric
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package telemetrytypes
|
||||
|
||||
// Enum returns the acceptable values for Signal.
|
||||
func (Signal) Enum() []any {
|
||||
return []any{
|
||||
SignalTraces,
|
||||
SignalLogs,
|
||||
SignalMetrics,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for FieldContext.
|
||||
func (FieldContext) Enum() []any {
|
||||
return []any{
|
||||
FieldContextMetric,
|
||||
FieldContextLog,
|
||||
FieldContextSpan,
|
||||
// FieldContextTrace,
|
||||
FieldContextResource,
|
||||
// FieldContextScope,
|
||||
FieldContextAttribute,
|
||||
// FieldContextEvent,
|
||||
FieldContextBody,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for Source.
|
||||
func (Source) Enum() []any {
|
||||
return []any{
|
||||
SourceMeter,
|
||||
}
|
||||
}
|
||||
|
||||
// Enum returns the acceptable values for FieldDataType.
|
||||
func (FieldDataType) Enum() []any {
|
||||
return []any{
|
||||
FieldDataTypeString,
|
||||
FieldDataTypeBool,
|
||||
FieldDataTypeFloat64,
|
||||
FieldDataTypeInt64,
|
||||
FieldDataTypeNumber,
|
||||
// FieldDataTypeArrayString,
|
||||
// FieldDataTypeArrayFloat64,
|
||||
// FieldDataTypeArrayBool,
|
||||
// FieldDataTypeArrayInt64,
|
||||
// FieldDataTypeArrayNumber,
|
||||
}
|
||||
}
|
||||
@@ -107,13 +107,3 @@ func (enum *Email) UnmarshalText(text []byte) error {
|
||||
func (enum Email) MarshalText() (text []byte, err error) {
|
||||
return []byte(enum.StringValue()), nil
|
||||
}
|
||||
|
||||
func (enum *Email) UnmarshalParam(param string) error {
|
||||
email, err := NewEmail(param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*enum = email
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -77,10 +77,3 @@ func (enum *String) UnmarshalText(text []byte) error {
|
||||
func (enum String) MarshalText() (text []byte, err error) {
|
||||
return []byte(enum.StringValue()), nil
|
||||
}
|
||||
|
||||
// Implement Gin's BindUnmarshaler interface
|
||||
// See https://github.com/SigNoz/signoz/pull/10219 description for additional details
|
||||
func (enum *String) UnmarshalParam(param string) error {
|
||||
*enum = NewString(param)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -140,13 +140,3 @@ func (enum *UUID) UnmarshalText(text []byte) error {
|
||||
func (enum UUID) MarshalText() (text []byte, err error) {
|
||||
return []byte(enum.StringValue()), nil
|
||||
}
|
||||
|
||||
func (enum *UUID) UnmarshalParam(param string) error {
|
||||
uuid, err := NewUUID(param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*enum = uuid
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
ginbinding "github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -43,7 +42,4 @@ type Valuer interface {
|
||||
|
||||
// Implement encoding.TextUnmarshaler to allow the value to be marshalled unto a string
|
||||
encoding.TextMarshaler
|
||||
|
||||
// Implement Gin's BindUnmarshaler interface
|
||||
ginbinding.BindUnmarshaler
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user