mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 03:35:52 +08:00
Merge branch 'develop' into chore.preferDeltaTemporalityByDefault
This commit is contained in:
commit
616e3af18f
2
.gitignore
vendored
2
.gitignore
vendored
@ -37,7 +37,7 @@ frontend/src/constants/env.ts
|
|||||||
**/locust-scripts/__pycache__/
|
**/locust-scripts/__pycache__/
|
||||||
**/__debug_bin
|
**/__debug_bin
|
||||||
|
|
||||||
frontend/*.env
|
frontend/.env
|
||||||
pkg/query-service/signoz.db
|
pkg/query-service/signoz.db
|
||||||
|
|
||||||
pkg/query-service/tests/test-deploy/data/
|
pkg/query-service/tests/test-deploy/data/
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.vscode
|
.vscode
|
||||||
build
|
build
|
||||||
.env
|
|
||||||
.git
|
.git
|
||||||
|
7
frontend/example.env
Normal file
7
frontend/example.env
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
NODE_ENV="development"
|
||||||
|
BUNDLE_ANALYSER="true"
|
||||||
|
FRONTEND_API_ENDPOINT="http://localhost:3301/"
|
||||||
|
INTERCOM_APP_ID="intercom-app-id"
|
||||||
|
|
||||||
|
PLAYWRIGHT_TEST_BASE_URL="http://localhost:3301"
|
||||||
|
CI="1"
|
@ -10,8 +10,9 @@ import { useQueryService } from 'hooks/useQueryService';
|
|||||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||||
import { PayloadProps as QueryServicePayloadProps } from 'types/api/metrics/getService';
|
import { PayloadProps as QueryServicePayloadProps } from 'types/api/metrics/getService';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { Tags } from 'types/reducer/trace';
|
import { Tags } from 'types/reducer/trace';
|
||||||
@ -22,12 +23,14 @@ interface ConnectionStatusProps {
|
|||||||
framework: string;
|
framework: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pollingInterval = 15000;
|
||||||
|
|
||||||
export default function ConnectionStatus({
|
export default function ConnectionStatus({
|
||||||
serviceName,
|
serviceName,
|
||||||
language,
|
language,
|
||||||
framework,
|
framework,
|
||||||
}: ConnectionStatusProps): JSX.Element {
|
}: ConnectionStatusProps): JSX.Element {
|
||||||
const { maxTime, minTime, selectedTime } = useSelector<
|
const { minTime, maxTime, selectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
@ -37,22 +40,23 @@ export default function ConnectionStatus({
|
|||||||
[queries],
|
[queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [pollingInterval, setPollingInterval] = useState<number | false>(15000); // initial Polling interval of 15 secs , Set to false after 5 mins
|
|
||||||
const [retryCount, setRetryCount] = useState(20); // Retry for 5 mins
|
const [retryCount, setRetryCount] = useState(20); // Retry for 5 mins
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isReceivingData, setIsReceivingData] = useState(false);
|
const [isReceivingData, setIsReceivingData] = useState(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { data, error, isFetching: isServiceLoading, isError } = useQueryService(
|
const {
|
||||||
{
|
data,
|
||||||
minTime,
|
error,
|
||||||
maxTime,
|
isFetching: isServiceLoading,
|
||||||
selectedTime,
|
isError,
|
||||||
selectedTags,
|
refetch,
|
||||||
options: {
|
} = useQueryService({
|
||||||
refetchInterval: pollingInterval,
|
minTime,
|
||||||
},
|
maxTime,
|
||||||
},
|
selectedTime,
|
||||||
);
|
selectedTags,
|
||||||
|
});
|
||||||
|
|
||||||
const renderDocsReference = (): JSX.Element => {
|
const renderDocsReference = (): JSX.Element => {
|
||||||
switch (language) {
|
switch (language) {
|
||||||
@ -107,10 +111,8 @@ export default function ConnectionStatus({
|
|||||||
const verifyApplicationData = (response?: QueryServicePayloadProps): void => {
|
const verifyApplicationData = (response?: QueryServicePayloadProps): void => {
|
||||||
if (data || isError) {
|
if (data || isError) {
|
||||||
setRetryCount(retryCount - 1);
|
setRetryCount(retryCount - 1);
|
||||||
|
|
||||||
if (retryCount < 0) {
|
if (retryCount < 0) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPollingInterval(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,11 +128,44 @@ export default function ConnectionStatus({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Use useEffect to update query parameters when the polling interval lapses
|
||||||
|
useEffect(() => {
|
||||||
|
const pollingTimer = setInterval(() => {
|
||||||
|
// Trigger a refetch with the updated parameters
|
||||||
|
const updatedMinTime = (Date.now() - 15 * 60 * 1000) * 1000000;
|
||||||
|
const updatedMaxTime = Date.now() * 1000000;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
maxTime: updatedMaxTime,
|
||||||
|
minTime: updatedMinTime,
|
||||||
|
selectedTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_TIME_INTERVAL,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
// refetch(updatedParams);
|
||||||
|
}, pollingInterval); // Same interval as pollingInterval
|
||||||
|
|
||||||
|
// Clean up the interval when the component unmounts
|
||||||
|
return (): void => {
|
||||||
|
clearInterval(pollingTimer);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [refetch, selectedTags, selectedTime]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
verifyApplicationData(data);
|
verifyApplicationData(data);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isServiceLoading, data, error, isError]);
|
}, [isServiceLoading, data, error, isError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetch();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="connection-status-container">
|
<div className="connection-status-container">
|
||||||
<div className="full-docs-link">{renderDocsReference()}</div>
|
<div className="full-docs-link">{renderDocsReference()}</div>
|
||||||
|
@ -5,7 +5,9 @@ import loginApi from 'api/user/login';
|
|||||||
import signUpApi from 'api/user/signup';
|
import signUpApi from 'api/user/signup';
|
||||||
import afterLogin from 'AppRoutes/utils';
|
import afterLogin from 'AppRoutes/utils';
|
||||||
import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
|
import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
|
||||||
|
import { FeatureKeys } from 'constants/features';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@ -57,6 +59,8 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
|||||||
const token = params.get('token');
|
const token = params.get('token');
|
||||||
const [isDetailsDisable, setIsDetailsDisable] = useState<boolean>(false);
|
const [isDetailsDisable, setIsDetailsDisable] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const isOnboardingEnabled = useFeatureFlag(FeatureKeys.ONBOARDING)?.active;
|
||||||
|
|
||||||
const getInviteDetailsResponse = useQuery({
|
const getInviteDetailsResponse = useQuery({
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
getInviteDetails({
|
getInviteDetails({
|
||||||
@ -237,7 +241,11 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
|||||||
await commonHandler(
|
await commonHandler(
|
||||||
values,
|
values,
|
||||||
async (): Promise<void> => {
|
async (): Promise<void> => {
|
||||||
history.push(ROUTES.APPLICATION);
|
if (isOnboardingEnabled) {
|
||||||
|
history.push(ROUTES.GET_STARTED);
|
||||||
|
} else {
|
||||||
|
history.push(ROUTES.APPLICATION);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ const plugins = [
|
|||||||
}),
|
}),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': JSON.stringify({
|
'process.env': JSON.stringify({
|
||||||
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
|
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
|
||||||
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
|
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
|
||||||
}),
|
}),
|
||||||
|
@ -5,6 +5,7 @@ const { resolve } = require('path');
|
|||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
|
const dotenv = require('dotenv');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
@ -13,6 +14,8 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|||||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||||
const Critters = require('critters-webpack-plugin');
|
const Critters = require('critters-webpack-plugin');
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
const cssLoader = 'css-loader';
|
const cssLoader = 'css-loader';
|
||||||
const sassLoader = 'sass-loader';
|
const sassLoader = 'sass-loader';
|
||||||
const styleLoader = 'style-loader';
|
const styleLoader = 'style-loader';
|
||||||
@ -31,6 +34,7 @@ const plugins = [
|
|||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': JSON.stringify({
|
'process.env': JSON.stringify({
|
||||||
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
|
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
|
||||||
|
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
new MiniCssExtractPlugin(),
|
new MiniCssExtractPlugin(),
|
||||||
|
@ -27,6 +27,9 @@ func EnrichmentRequired(params *v3.QueryRangeParamsV3) bool {
|
|||||||
// check filter attribute
|
// check filter attribute
|
||||||
if query.Filters != nil && len(query.Filters.Items) != 0 {
|
if query.Filters != nil && len(query.Filters.Items) != 0 {
|
||||||
for _, item := range query.Filters.Items {
|
for _, item := range query.Filters.Items {
|
||||||
|
if item.Key.IsJSON {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !isEnriched(item.Key) {
|
if !isEnriched(item.Key) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -97,6 +100,9 @@ func enrichLogsQuery(query *v3.BuilderQuery, fields map[string]v3.AttributeKey)
|
|||||||
// enrich filter attribute
|
// enrich filter attribute
|
||||||
if query.Filters != nil && len(query.Filters.Items) != 0 {
|
if query.Filters != nil && len(query.Filters.Items) != 0 {
|
||||||
for i := 0; i < len(query.Filters.Items); i++ {
|
for i := 0; i < len(query.Filters.Items); i++ {
|
||||||
|
if query.Filters.Items[i].Key.IsJSON {
|
||||||
|
continue
|
||||||
|
}
|
||||||
query.Filters.Items[i].Key = enrichFieldWithMetadata(query.Filters.Items[i].Key, fields)
|
query.Filters.Items[i].Key = enrichFieldWithMetadata(query.Filters.Items[i].Key, fields)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,24 @@ var testEnrichmentRequiredData = []struct {
|
|||||||
},
|
},
|
||||||
EnrichmentRequired: true,
|
EnrichmentRequired: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "filter enrichment not required required json",
|
||||||
|
Params: v3.QueryRangeParamsV3{
|
||||||
|
CompositeQuery: &v3.CompositeQuery{
|
||||||
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||||
|
"test": {
|
||||||
|
QueryName: "test",
|
||||||
|
Expression: "test",
|
||||||
|
DataSource: v3.DataSourceLogs,
|
||||||
|
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||||
|
{Key: v3.AttributeKey{Key: "user_name", IsJSON: true}, Value: "john", Operator: "="},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EnrichmentRequired: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "groupBy enrichment not required",
|
Name: "groupBy enrichment not required",
|
||||||
Params: v3.QueryRangeParamsV3{
|
Params: v3.QueryRangeParamsV3{
|
||||||
|
122
pkg/query-service/app/logs/v3/json_filter.go
Normal file
122
pkg/query-service/app/logs/v3/json_filter.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package v3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
STRING = "String"
|
||||||
|
INT64 = "Int64"
|
||||||
|
FLOAT64 = "Float64"
|
||||||
|
ARRAY_STRING = "Array(String)"
|
||||||
|
ARRAY_INT64 = "Array(Int64)"
|
||||||
|
ARRAY_FLOAT64 = "Array(Float64)"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dataTypeMapping = map[string]string{
|
||||||
|
"string": STRING,
|
||||||
|
"int64": INT64,
|
||||||
|
"float64": FLOAT64,
|
||||||
|
"array(string)": ARRAY_STRING,
|
||||||
|
"array(int64)": ARRAY_INT64,
|
||||||
|
"array(float64)": ARRAY_FLOAT64,
|
||||||
|
}
|
||||||
|
|
||||||
|
var arrayValueTypeMapping = map[string]string{
|
||||||
|
"array(string)": "string",
|
||||||
|
"array(int64)": "int64",
|
||||||
|
"array(float64)": "float64",
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonLogOperators = map[v3.FilterOperator]string{
|
||||||
|
v3.FilterOperatorEqual: "=",
|
||||||
|
v3.FilterOperatorNotEqual: "!=",
|
||||||
|
v3.FilterOperatorLessThan: "<",
|
||||||
|
v3.FilterOperatorLessThanOrEq: "<=",
|
||||||
|
v3.FilterOperatorGreaterThan: ">",
|
||||||
|
v3.FilterOperatorGreaterThanOrEq: ">=",
|
||||||
|
v3.FilterOperatorLike: "ILIKE",
|
||||||
|
v3.FilterOperatorNotLike: "NOT ILIKE",
|
||||||
|
v3.FilterOperatorContains: "ILIKE",
|
||||||
|
v3.FilterOperatorNotContains: "NOT ILIKE",
|
||||||
|
v3.FilterOperatorRegex: "match(%s, %s)",
|
||||||
|
v3.FilterOperatorNotRegex: "NOT match(%s, %s)",
|
||||||
|
v3.FilterOperatorIn: "IN",
|
||||||
|
v3.FilterOperatorNotIn: "NOT IN",
|
||||||
|
v3.FilterOperatorHas: "has(%s, %s)",
|
||||||
|
v3.FilterOperatorNotHas: "NOT has(%s, %s)",
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJSONFilterKey(key v3.AttributeKey, isArray bool) (string, error) {
|
||||||
|
keyArr := strings.Split(key.Key, ".")
|
||||||
|
if len(keyArr) < 2 {
|
||||||
|
return "", fmt.Errorf("incorrect key, should contain at least 2 parts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// only body is supported as of now
|
||||||
|
if strings.Compare(keyArr[0], "body") != 0 {
|
||||||
|
return "", fmt.Errorf("only body can be the root key")
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataType string
|
||||||
|
var ok bool
|
||||||
|
if dataType, ok = dataTypeMapping[string(key.DataType)]; !ok {
|
||||||
|
return "", fmt.Errorf("unsupported dataType for JSON: %s", key.DataType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isArray {
|
||||||
|
return fmt.Sprintf("JSONExtract(JSON_QUERY(%s, '$.%s'), '%s')", keyArr[0], strings.Join(keyArr[1:], "."), dataType), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// for non array
|
||||||
|
keyname := fmt.Sprintf("JSON_VALUE(%s, '$.%s')", keyArr[0], strings.Join(keyArr[1:], "."))
|
||||||
|
if dataType != "String" {
|
||||||
|
keyname = fmt.Sprintf("JSONExtract(%s, '%s')", keyname, dataType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyname, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetJSONFilter(item v3.FilterItem) (string, error) {
|
||||||
|
|
||||||
|
dataType := item.Key.DataType
|
||||||
|
isArray := false
|
||||||
|
// check if its an array and handle it
|
||||||
|
if val, ok := arrayValueTypeMapping[string(item.Key.DataType)]; ok {
|
||||||
|
if item.Operator != v3.FilterOperatorHas && item.Operator != v3.FilterOperatorNotHas {
|
||||||
|
return "", fmt.Errorf("only has operator is supported for array")
|
||||||
|
}
|
||||||
|
isArray = true
|
||||||
|
dataType = v3.AttributeKeyDataType(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := getJSONFilterKey(item.Key, isArray)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// non array
|
||||||
|
value, err := utils.ValidateAndCastValue(item.Value, dataType)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to validate and cast value for %s: %v", item.Key.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator))))
|
||||||
|
if logsOp, ok := jsonLogOperators[op]; ok {
|
||||||
|
switch op {
|
||||||
|
case v3.FilterOperatorRegex, v3.FilterOperatorNotRegex, v3.FilterOperatorHas, v3.FilterOperatorNotHas:
|
||||||
|
fmtVal := utils.ClickHouseFormattedValue(value)
|
||||||
|
return fmt.Sprintf(logsOp, key, fmtVal), nil
|
||||||
|
case v3.FilterOperatorContains, v3.FilterOperatorNotContains:
|
||||||
|
return fmt.Sprintf("%s %s '%%%s%%'", key, logsOp, item.Value), nil
|
||||||
|
default:
|
||||||
|
fmtVal := utils.ClickHouseFormattedValue(value)
|
||||||
|
return fmt.Sprintf("%s %s %s", key, logsOp, fmtVal), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("unsupported operator: %s", op)
|
||||||
|
}
|
260
pkg/query-service/app/logs/v3/json_filter_test.go
Normal file
260
pkg/query-service/app/logs/v3/json_filter_test.go
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
package v3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testGetJSONFilterKeyData = []struct {
|
||||||
|
Name string
|
||||||
|
Key v3.AttributeKey
|
||||||
|
IsArray bool
|
||||||
|
ClickhouseKey string
|
||||||
|
Error bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "Incorrect Key",
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "requestor_list[*]",
|
||||||
|
DataType: "array(string)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
IsArray: true,
|
||||||
|
Error: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Using anything other than body",
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "trace_id.requestor_list[*]",
|
||||||
|
DataType: "array(string)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
IsArray: true,
|
||||||
|
Error: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Array String",
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.requestor_list[*]",
|
||||||
|
DataType: "array(string)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
IsArray: true,
|
||||||
|
ClickhouseKey: "JSONExtract(JSON_QUERY(body, '$.requestor_list[*]'), '" + ARRAY_STRING + "')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Array String Nested",
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.nested[*].key[*]",
|
||||||
|
DataType: "array(string)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
IsArray: true,
|
||||||
|
ClickhouseKey: "JSONExtract(JSON_QUERY(body, '$.nested[*].key[*]'), '" + ARRAY_STRING + "')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Array Int",
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.int_numbers[*]",
|
||||||
|
DataType: "array(int64)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
IsArray: true,
|
||||||
|
ClickhouseKey: "JSONExtract(JSON_QUERY(body, '$.int_numbers[*]'), '" + ARRAY_INT64 + "')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Array Float",
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.nested_num[*].float_nums[*]",
|
||||||
|
DataType: "array(float64)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
IsArray: true,
|
||||||
|
ClickhouseKey: "JSONExtract(JSON_QUERY(body, '$.nested_num[*].float_nums[*]'), '" + ARRAY_FLOAT64 + "')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "String",
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.message",
|
||||||
|
DataType: "string",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
IsArray: false,
|
||||||
|
ClickhouseKey: "JSON_VALUE(body, '$.message')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Int",
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.status",
|
||||||
|
DataType: "int64",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
IsArray: false,
|
||||||
|
ClickhouseKey: "JSONExtract(JSON_VALUE(body, '$.status'), '" + INT64 + "')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Float",
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.fraction",
|
||||||
|
DataType: "float64",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
IsArray: false,
|
||||||
|
ClickhouseKey: "JSONExtract(JSON_VALUE(body, '$.fraction'), '" + FLOAT64 + "')",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetJSONFilterKey(t *testing.T) {
|
||||||
|
for _, tt := range testGetJSONFilterKeyData {
|
||||||
|
Convey("testgetKey", t, func() {
|
||||||
|
columnName, err := getJSONFilterKey(tt.Key, tt.IsArray)
|
||||||
|
if tt.Error {
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
} else {
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(columnName, ShouldEqual, tt.ClickhouseKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testGetJSONFilterData = []struct {
|
||||||
|
Name string
|
||||||
|
FilterItem v3.FilterItem
|
||||||
|
Filter string
|
||||||
|
Error bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "Array membership string",
|
||||||
|
FilterItem: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.requestor_list[*]",
|
||||||
|
DataType: "array(string)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "has",
|
||||||
|
Value: "index_service",
|
||||||
|
},
|
||||||
|
Filter: "has(JSONExtract(JSON_QUERY(body, '$.requestor_list[*]'), 'Array(String)'), 'index_service')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Array membership int64",
|
||||||
|
FilterItem: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.int_numbers[*]",
|
||||||
|
DataType: "array(int64)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "has",
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
Filter: "has(JSONExtract(JSON_QUERY(body, '$.int_numbers[*]'), '" + ARRAY_INT64 + "'), 2)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Array membership float64",
|
||||||
|
FilterItem: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.nested_num[*].float_nums[*]",
|
||||||
|
DataType: "array(float64)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "nhas",
|
||||||
|
Value: 2.2,
|
||||||
|
},
|
||||||
|
Filter: "NOT has(JSONExtract(JSON_QUERY(body, '$.nested_num[*].float_nums[*]'), '" + ARRAY_FLOAT64 + "'), 2.200000)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "eq operator",
|
||||||
|
FilterItem: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.message",
|
||||||
|
DataType: "string",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "=",
|
||||||
|
Value: "hello",
|
||||||
|
},
|
||||||
|
Filter: "JSON_VALUE(body, '$.message') = 'hello'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "eq operator number",
|
||||||
|
FilterItem: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.status",
|
||||||
|
DataType: "int64",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "=",
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
Filter: "JSONExtract(JSON_VALUE(body, '$.status'), '" + INT64 + "') = 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "neq operator number",
|
||||||
|
FilterItem: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.status",
|
||||||
|
DataType: "float64",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "=",
|
||||||
|
Value: 1.1,
|
||||||
|
},
|
||||||
|
Filter: "JSONExtract(JSON_VALUE(body, '$.status'), '" + FLOAT64 + "') = 1.100000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "greater than operator",
|
||||||
|
FilterItem: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.status",
|
||||||
|
DataType: "int64",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: ">",
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
Filter: "JSONExtract(JSON_VALUE(body, '$.status'), '" + INT64 + "') > 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "regex operator",
|
||||||
|
FilterItem: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.message",
|
||||||
|
DataType: "string",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "regex",
|
||||||
|
Value: "a*",
|
||||||
|
},
|
||||||
|
Filter: "match(JSON_VALUE(body, '$.message'), 'a*')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "contains operator",
|
||||||
|
FilterItem: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.message",
|
||||||
|
DataType: "string",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "contains",
|
||||||
|
Value: "a",
|
||||||
|
},
|
||||||
|
Filter: "JSON_VALUE(body, '$.message') ILIKE '%a%'",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetJSONFilter(t *testing.T) {
|
||||||
|
for _, tt := range testGetJSONFilterData {
|
||||||
|
Convey("testGetJSONFilter", t, func() {
|
||||||
|
filter, err := GetJSONFilter(tt.FilterItem)
|
||||||
|
if tt.Error {
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
} else {
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(filter, ShouldEqual, tt.Filter)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -49,7 +49,6 @@ var logOperators = map[v3.FilterOperator]string{
|
|||||||
v3.FilterOperatorNotIn: "NOT IN",
|
v3.FilterOperatorNotIn: "NOT IN",
|
||||||
v3.FilterOperatorExists: "has(%s_%s_key, '%s')",
|
v3.FilterOperatorExists: "has(%s_%s_key, '%s')",
|
||||||
v3.FilterOperatorNotExists: "not has(%s_%s_key, '%s')",
|
v3.FilterOperatorNotExists: "not has(%s_%s_key, '%s')",
|
||||||
// (todo) check contains/not contains/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string {
|
func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string {
|
||||||
@ -161,6 +160,15 @@ func buildLogsTimeSeriesFilterQuery(fs *v3.FilterSet, groupBy []v3.AttributeKey,
|
|||||||
|
|
||||||
if fs != nil && len(fs.Items) != 0 {
|
if fs != nil && len(fs.Items) != 0 {
|
||||||
for _, item := range fs.Items {
|
for _, item := range fs.Items {
|
||||||
|
if item.Key.IsJSON {
|
||||||
|
filter, err := GetJSONFilter(item)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
conditions = append(conditions, filter)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator))))
|
op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator))))
|
||||||
|
|
||||||
var value interface{}
|
var value interface{}
|
||||||
|
@ -876,6 +876,74 @@ var testBuildLogsQueryData = []struct {
|
|||||||
TableName: "logs",
|
TableName: "logs",
|
||||||
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as name, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND indexOf(attributes_string_key, 'name') > 0 group by name order by name DESC",
|
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as name, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND indexOf(attributes_string_key, 'name') > 0 group by name order by name DESC",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "TABLE: Test count with JSON Filter, groupBy, orderBy",
|
||||||
|
PanelType: v3.PanelTypeTable,
|
||||||
|
Start: 1680066360726210000,
|
||||||
|
End: 1680066458000000000,
|
||||||
|
BuilderQuery: &v3.BuilderQuery{
|
||||||
|
QueryName: "A",
|
||||||
|
StepInterval: 60,
|
||||||
|
AggregateOperator: v3.AggregateOperatorCount,
|
||||||
|
Expression: "A",
|
||||||
|
Filters: &v3.FilterSet{
|
||||||
|
Operator: "AND",
|
||||||
|
Items: []v3.FilterItem{
|
||||||
|
{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.message",
|
||||||
|
DataType: "string",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "contains",
|
||||||
|
Value: "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GroupBy: []v3.AttributeKey{
|
||||||
|
{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
||||||
|
},
|
||||||
|
OrderBy: []v3.OrderBy{
|
||||||
|
{ColumnName: "name", Order: "DESC"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TableName: "logs",
|
||||||
|
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as name, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND JSON_VALUE(body, '$.message') ILIKE '%a%' AND indexOf(attributes_string_key, 'name') > 0 group by name order by name DESC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TABLE: Test count with JSON Filter Array, groupBy, orderBy",
|
||||||
|
PanelType: v3.PanelTypeTable,
|
||||||
|
Start: 1680066360726210000,
|
||||||
|
End: 1680066458000000000,
|
||||||
|
BuilderQuery: &v3.BuilderQuery{
|
||||||
|
QueryName: "A",
|
||||||
|
StepInterval: 60,
|
||||||
|
AggregateOperator: v3.AggregateOperatorCount,
|
||||||
|
Expression: "A",
|
||||||
|
Filters: &v3.FilterSet{
|
||||||
|
Operator: "AND",
|
||||||
|
Items: []v3.FilterItem{
|
||||||
|
{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body.requestor_list[*]",
|
||||||
|
DataType: "array(string)",
|
||||||
|
IsJSON: true,
|
||||||
|
},
|
||||||
|
Operator: "has",
|
||||||
|
Value: "index_service",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GroupBy: []v3.AttributeKey{
|
||||||
|
{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
||||||
|
},
|
||||||
|
OrderBy: []v3.OrderBy{
|
||||||
|
{ColumnName: "name", Order: "DESC"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TableName: "logs",
|
||||||
|
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as name, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND has(JSONExtract(JSON_QUERY(body, '$.requestor_list[*]'), 'Array(String)'), 'index_service') AND indexOf(attributes_string_key, 'name') > 0 group by name order by name DESC",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildLogsQuery(t *testing.T) {
|
func TestBuildLogsQuery(t *testing.T) {
|
||||||
|
@ -234,11 +234,14 @@ type FilterAttributeKeyRequest struct {
|
|||||||
type AttributeKeyDataType string
|
type AttributeKeyDataType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AttributeKeyDataTypeUnspecified AttributeKeyDataType = ""
|
AttributeKeyDataTypeUnspecified AttributeKeyDataType = ""
|
||||||
AttributeKeyDataTypeString AttributeKeyDataType = "string"
|
AttributeKeyDataTypeString AttributeKeyDataType = "string"
|
||||||
AttributeKeyDataTypeInt64 AttributeKeyDataType = "int64"
|
AttributeKeyDataTypeInt64 AttributeKeyDataType = "int64"
|
||||||
AttributeKeyDataTypeFloat64 AttributeKeyDataType = "float64"
|
AttributeKeyDataTypeFloat64 AttributeKeyDataType = "float64"
|
||||||
AttributeKeyDataTypeBool AttributeKeyDataType = "bool"
|
AttributeKeyDataTypeBool AttributeKeyDataType = "bool"
|
||||||
|
AttributeKeyDataTypeArrayString AttributeKeyDataType = "array(string)"
|
||||||
|
AttributeKeyDataTypeArrayInt64 AttributeKeyDataType = "array(int64)"
|
||||||
|
AttributeKeyDataTypeArrayFloat64 AttributeKeyDataType = "array(float64)"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (q AttributeKeyDataType) Validate() error {
|
func (q AttributeKeyDataType) Validate() error {
|
||||||
@ -285,6 +288,7 @@ type AttributeKey struct {
|
|||||||
DataType AttributeKeyDataType `json:"dataType"`
|
DataType AttributeKeyDataType `json:"dataType"`
|
||||||
Type AttributeKeyType `json:"type"`
|
Type AttributeKeyType `json:"type"`
|
||||||
IsColumn bool `json:"isColumn"`
|
IsColumn bool `json:"isColumn"`
|
||||||
|
IsJSON bool `json:"isJSON"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AttributeKey) CacheKey() string {
|
func (a AttributeKey) CacheKey() string {
|
||||||
@ -293,7 +297,7 @@ func (a AttributeKey) CacheKey() string {
|
|||||||
|
|
||||||
func (a AttributeKey) Validate() error {
|
func (a AttributeKey) Validate() error {
|
||||||
switch a.DataType {
|
switch a.DataType {
|
||||||
case AttributeKeyDataTypeBool, AttributeKeyDataTypeInt64, AttributeKeyDataTypeFloat64, AttributeKeyDataTypeString, AttributeKeyDataTypeUnspecified:
|
case AttributeKeyDataTypeBool, AttributeKeyDataTypeInt64, AttributeKeyDataTypeFloat64, AttributeKeyDataTypeString, AttributeKeyDataTypeArrayFloat64, AttributeKeyDataTypeArrayString, AttributeKeyDataTypeArrayInt64, AttributeKeyDataTypeUnspecified:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid attribute dataType: %s", a.DataType)
|
return fmt.Errorf("invalid attribute dataType: %s", a.DataType)
|
||||||
@ -545,6 +549,9 @@ const (
|
|||||||
|
|
||||||
FilterOperatorExists FilterOperator = "exists"
|
FilterOperatorExists FilterOperator = "exists"
|
||||||
FilterOperatorNotExists FilterOperator = "nexists"
|
FilterOperatorNotExists FilterOperator = "nexists"
|
||||||
|
|
||||||
|
FilterOperatorHas FilterOperator = "has"
|
||||||
|
FilterOperatorNotHas FilterOperator = "nhas"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FilterItem struct {
|
type FilterItem struct {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user