Merge branch 'develop' into SIG-5729

This commit is contained in:
rahulkeswani101 2024-09-26 17:28:49 +05:30 committed by GitHub
commit c7bd7566c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 302 additions and 79 deletions

View File

@ -33,7 +33,14 @@ export const Card = styled(CardComponent)<CardProps>`
} }
.ant-card-body { .ant-card-body {
${({ $panelType }): StyledCSS =>
$panelType === PANEL_TYPES.TABLE
? css`
height: 100%;
`
: css`
height: calc(100% - 30px); height: calc(100% - 30px);
`}
padding: 0; padding: 0;
} }
`; `;

View File

@ -63,6 +63,8 @@
height: 40px; height: 40px;
justify-content: end; justify-content: end;
padding: 0 8px; padding: 0 8px;
margin-top: 12px;
margin-bottom: 2px;
} }
} }

View File

@ -43,6 +43,15 @@
.ant-select-item { .ant-select-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
}
.rc-virtual-list-holder {
[data-testid='option-ALL'] {
border-bottom: 1px solid var(--bg-slate-400);
padding-bottom: 12px;
margin-bottom: 8px;
}
} }
.all-label { .all-label {
@ -56,28 +65,25 @@
} }
.dropdown-value { .dropdown-value {
display: flex; display: grid;
justify-content: space-between; grid-template-columns: 1fr max-content;
align-items: center;
.option-text { .option-text {
max-width: 180px;
padding: 0 8px; padding: 0 8px;
} }
.toggle-tag-label { .toggle-tag-label {
padding-left: 8px; padding-left: 8px;
right: 40px; right: 40px;
font-weight: normal; font-weight: 500;
position: absolute;
} }
} }
} }
} }
.dropdown-styles { .dropdown-styles {
min-width: 300px; min-width: 400px;
max-width: 350px; max-width: 500px;
} }
.lightMode { .lightMode {

View File

@ -62,14 +62,14 @@ interface VariableItemProps {
const getSelectValue = ( const getSelectValue = (
selectedValue: IDashboardVariable['selectedValue'], selectedValue: IDashboardVariable['selectedValue'],
variableData: IDashboardVariable, variableData: IDashboardVariable,
): string | string[] => { ): string | string[] | undefined => {
if (Array.isArray(selectedValue)) { if (Array.isArray(selectedValue)) {
if (!variableData.multiSelect && selectedValue.length === 1) { if (!variableData.multiSelect && selectedValue.length === 1) {
return selectedValue[0]?.toString() || ''; return selectedValue[0]?.toString();
} }
return selectedValue.map((item) => item.toString()); return selectedValue.map((item) => item.toString());
} }
return selectedValue?.toString() || ''; return selectedValue?.toString();
}; };
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
@ -300,7 +300,7 @@ function VariableItem({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
const isChecked = const isChecked =
variableData.allSelected || selectValue.includes(ALL_SELECT_VALUE); variableData.allSelected || selectValue?.includes(ALL_SELECT_VALUE);
if (isChecked) { if (isChecked) {
handleChange([]); handleChange([]);
@ -462,6 +462,7 @@ function VariableItem({
<span>+ {omittedValues.length} </span> <span>+ {omittedValues.length} </span>
</Tooltip> </Tooltip>
)} )}
allowClear
> >
{enableSelectAll && ( {enableSelectAll && (
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}> <Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
@ -500,11 +501,17 @@ function VariableItem({
{...retProps(option as string)} {...retProps(option as string)}
onClick={(e): void => handleToggle(e as any, option as string)} onClick={(e): void => handleToggle(e as any, option as string)}
> >
<Tooltip title={option.toString()} placement="bottomRight"> <Typography.Text
<Typography.Text ellipsis className="option-text"> ellipsis={{
tooltip: {
placement: variableData.multiSelect ? 'top' : 'right',
autoAdjustOverflow: true,
},
}}
className="option-text"
>
{option.toString()} {option.toString()}
</Typography.Text> </Typography.Text>
</Tooltip>
{variableData.multiSelect && {variableData.multiSelect &&
optionState.tag === option.toString() && optionState.tag === option.toString() &&

View File

@ -1,3 +1,4 @@
import { Color } from '@signozhq/design-tokens';
import { Select } from 'antd'; import { Select } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app'; import { ENTITY_VERSION_V4 } from 'constants/app';
// ** Constants // ** Constants
@ -34,6 +35,7 @@ export function HavingFilter({
const [currentFormValue, setCurrentFormValue] = useState<HavingForm>( const [currentFormValue, setCurrentFormValue] = useState<HavingForm>(
initialHavingValues, initialHavingValues,
); );
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { isMulti } = useTagValidation( const { isMulti } = useTagValidation(
currentFormValue.op, currentFormValue.op,
@ -198,6 +200,29 @@ export function HavingFilter({
resetChanges(); resetChanges();
}; };
const handleFocus = useCallback(() => {
setErrorMessage(null);
}, []);
const handleBlur = useCallback((): void => {
if (searchText) {
const { columnName, op, value } = getHavingObject(searchText);
const isCompleteHavingClause =
columnName && op && value.every((v) => v !== '');
if (isCompleteHavingClause && isValidHavingValue(searchText)) {
setLocalValues((prev) => {
const updatedValues = [...prev, searchText];
onChange(updatedValues.map(transformFromStringToHaving));
return updatedValues;
});
setSearchText('');
} else {
setErrorMessage('Invalid HAVING clause');
}
}
}, [searchText, onChange]);
useEffect(() => { useEffect(() => {
parseSearchText(searchText); parseSearchText(searchText);
}, [searchText, parseSearchText]); }, [searchText, parseSearchText]);
@ -209,6 +234,7 @@ export function HavingFilter({
const isMetricsDataSource = query.dataSource === DataSource.METRICS; const isMetricsDataSource = query.dataSource === DataSource.METRICS;
return ( return (
<>
<Select <Select
getPopupContainer={popupContainer} getPopupContainer={popupContainer}
autoClearSearchValue={false} autoClearSearchValue={false}
@ -225,6 +251,9 @@ export function HavingFilter({
onDeselect={handleDeselect} onDeselect={handleDeselect}
onChange={handleChange} onChange={handleChange}
onSelect={handleSelect} onSelect={handleSelect}
onFocus={handleFocus}
onBlur={handleBlur}
status={errorMessage ? 'error' : undefined}
> >
{options.map((opt) => ( {options.map((opt) => (
<Select.Option key={opt.value} value={opt.value} title="havingOption"> <Select.Option key={opt.value} value={opt.value} title="havingOption">
@ -232,5 +261,9 @@ export function HavingFilter({
</Select.Option> </Select.Option>
))} ))}
</Select> </Select>
{errorMessage && (
<div style={{ color: Color.BG_CHERRY_500 }}>{errorMessage}</div>
)}
</>
); );
} }

View File

@ -286,6 +286,32 @@ function QueryBuilderSearchV2(
parsedValue = value; parsedValue = value;
} }
if (currentState === DropdownState.ATTRIBUTE_KEY) { if (currentState === DropdownState.ATTRIBUTE_KEY) {
// Case - convert abc def ghi type attribute keys directly to body contains abc def ghi
if (
isObject(parsedValue) &&
parsedValue?.key &&
parsedValue?.key?.split(' ').length > 1
) {
setTags((prev) => [
...prev,
{
key: {
key: 'body',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
// eslint-disable-next-line sonarjs/no-duplicate-string
id: 'body--string----true',
},
op: OPERATORS.CONTAINS,
value: (parsedValue as BaseAutocompleteData)?.key,
},
]);
setCurrentFilterItem(undefined);
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else {
setCurrentFilterItem((prev) => ({ setCurrentFilterItem((prev) => ({
...prev, ...prev,
key: parsedValue as BaseAutocompleteData, key: parsedValue as BaseAutocompleteData,
@ -294,8 +320,28 @@ function QueryBuilderSearchV2(
})); }));
setCurrentState(DropdownState.OPERATOR); setCurrentState(DropdownState.OPERATOR);
setSearchValue((parsedValue as BaseAutocompleteData)?.key); setSearchValue((parsedValue as BaseAutocompleteData)?.key);
}
} else if (currentState === DropdownState.OPERATOR) { } else if (currentState === DropdownState.OPERATOR) {
if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) { if (isEmpty(value) && currentFilterItem?.key?.key) {
setTags((prev) => [
...prev,
{
key: {
key: 'body',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'body--string----true',
},
op: OPERATORS.CONTAINS,
value: currentFilterItem?.key?.key,
},
]);
setCurrentFilterItem(undefined);
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) {
setTags((prev) => [ setTags((prev) => [
...prev, ...prev,
{ {
@ -399,6 +445,7 @@ function QueryBuilderSearchV2(
whereClauseConfig?.customKey === 'body' && whereClauseConfig?.customKey === 'body' &&
whereClauseConfig?.customOp === OPERATORS.CONTAINS whereClauseConfig?.customOp === OPERATORS.CONTAINS
) { ) {
// eslint-disable-next-line sonarjs/no-identical-functions
setTags((prev) => [ setTags((prev) => [
...prev, ...prev,
{ {
@ -519,19 +566,20 @@ function QueryBuilderSearchV2(
setCurrentState(DropdownState.OPERATOR); setCurrentState(DropdownState.OPERATOR);
} }
} }
if (suggestionsData?.payload?.attributes?.length === 0) { // again let's not auto select anything for the user
if (tagOperator) {
setCurrentFilterItem({ setCurrentFilterItem({
key: { key: {
key: tagKey.split(' ')[0], key: tagKey,
dataType: DataTypes.EMPTY, dataType: DataTypes.EMPTY,
type: '', type: '',
isColumn: false, isColumn: false,
isJSON: false, isJSON: false,
}, },
op: '', op: tagOperator,
value: '', value: '',
}); });
setCurrentState(DropdownState.OPERATOR); setCurrentState(DropdownState.ATTRIBUTE_VALUE);
} }
} else if ( } else if (
// Case 2 - if key is defined but the search text doesn't match with the set key, // Case 2 - if key is defined but the search text doesn't match with the set key,
@ -607,13 +655,32 @@ function QueryBuilderSearchV2(
// the useEffect takes care of setting the dropdown values correctly on change of the current state // the useEffect takes care of setting the dropdown values correctly on change of the current state
useEffect(() => { useEffect(() => {
if (currentState === DropdownState.ATTRIBUTE_KEY) { if (currentState === DropdownState.ATTRIBUTE_KEY) {
const { tagKey } = getTagToken(searchValue);
if (isLogsExplorerPage) { if (isLogsExplorerPage) {
setDropdownOptions( // add the user typed option in the dropdown to select that and move ahead irrespective of the matches and all
suggestionsData?.payload?.attributes?.map((key) => ({ setDropdownOptions([
...(!isEmpty(tagKey) &&
!suggestionsData?.payload?.attributes?.some((val) =>
isEqual(val.key, tagKey),
)
? [
{
label: tagKey,
value: {
key: tagKey,
dataType: DataTypes.EMPTY,
type: '',
isColumn: false,
isJSON: false,
},
},
]
: []),
...(suggestionsData?.payload?.attributes?.map((key) => ({
label: key.key, label: key.key,
value: key, value: key,
})) || [], })) || []),
); ]);
} else { } else {
setDropdownOptions( setDropdownOptions(
data?.payload?.attributeKeys?.map((key) => ({ data?.payload?.attributeKeys?.map((key) => ({
@ -643,12 +710,14 @@ function QueryBuilderSearchV2(
op.label.startsWith(partialOperator.toLocaleUpperCase()), op.label.startsWith(partialOperator.toLocaleUpperCase()),
); );
} }
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions); setDropdownOptions(operatorOptions);
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) { } else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({ operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
label: operator, label: operator,
value: operator, value: operator,
})); }));
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions); setDropdownOptions(operatorOptions);
} else { } else {
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map( operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
@ -663,6 +732,7 @@ function QueryBuilderSearchV2(
op.label.startsWith(partialOperator.toLocaleUpperCase()), op.label.startsWith(partialOperator.toLocaleUpperCase()),
); );
} }
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions); setDropdownOptions(operatorOptions);
} }
} }

View File

@ -52,6 +52,8 @@
height: 40px; height: 40px;
justify-content: end; justify-content: end;
padding: 0 8px; padding: 0 8px;
margin-top: 12px;
margin-bottom: 2px;
} }
} }

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.23.2 github.com/ClickHouse/clickhouse-go/v2 v2.23.2
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.102.2 github.com/SigNoz/signoz-otel-collector v0.102.10
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
github.com/antonmedv/expr v1.15.3 github.com/antonmedv/expr v1.15.3

2
go.sum
View File

@ -66,6 +66,8 @@ github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRt
github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I= github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I=
github.com/SigNoz/signoz-otel-collector v0.102.2 h1:SmjsBZjMjTVVpuOlfJXlsDJQbdefQP/9Wz3CyzSuZuU= github.com/SigNoz/signoz-otel-collector v0.102.2 h1:SmjsBZjMjTVVpuOlfJXlsDJQbdefQP/9Wz3CyzSuZuU=
github.com/SigNoz/signoz-otel-collector v0.102.2/go.mod h1:ISAXYhZenojCWg6CdDJtPMpfS6Zwc08+uoxH25tc6Y0= github.com/SigNoz/signoz-otel-collector v0.102.2/go.mod h1:ISAXYhZenojCWg6CdDJtPMpfS6Zwc08+uoxH25tc6Y0=
github.com/SigNoz/signoz-otel-collector v0.102.10 h1:1zjU31OcRZL6fS0IIag8LA8bdhP4S28dzovDwuOg7Lg=
github.com/SigNoz/signoz-otel-collector v0.102.10/go.mod h1:APoBVD4aRu9vIny1vdzZSi2wPY3elyjHA/I/rh1hKfs=
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc= github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo= github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY= github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=

View File

@ -8,6 +8,7 @@ import (
"slices" "slices"
"strings" "strings"
"github.com/SigNoz/signoz-otel-collector/exporter/clickhouselogsexporter/logsv2"
"go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.uber.org/zap" "go.uber.org/zap"
@ -36,26 +37,7 @@ func (r *ClickHouseReader) GetQBFilterSuggestionsForLogs(
suggestions.AttributeKeys = attribKeysResp.AttributeKeys suggestions.AttributeKeys = attribKeysResp.AttributeKeys
// Rank suggested attributes // Rank suggested attributes
slices.SortFunc(suggestions.AttributeKeys, func(a v3.AttributeKey, b v3.AttributeKey) int { attribRanker.sort(suggestions.AttributeKeys)
// Higher score => higher rank
attribKeyScore := func(a v3.AttributeKey) int {
// Scoring criteria is expected to get more sophisticated in follow up changes
if a.Type == v3.AttributeKeyTypeResource {
return 2
}
if a.Type == v3.AttributeKeyTypeTag {
return 1
}
return 0
}
// To sort in descending order of score the return value must be negative when a > b
return attribKeyScore(b) - attribKeyScore(a)
})
// Put together suggested example queries. // Put together suggested example queries.
@ -268,3 +250,59 @@ func (r *ClickHouseReader) getValuesForLogAttributes(
return result, nil return result, nil
} }
var attribRanker = newRankingStrategy()
func newRankingStrategy() attribRankingStrategy {
// Some special resource attributes should get ranked above all others.
interestingResourceAttrsInDescRank := []string{
"service", "service.name", "env", "k8s.namespace.name",
}
// Synonyms of interesting attributes should come next
resourceHierarchy := logsv2.ResourceHierarchy()
for _, attr := range []string{
"service.name",
"deployment.environment",
"k8s.namespace.name",
"k8s.pod.name",
"k8s.container.name",
"k8s.node.name",
} {
interestingResourceAttrsInDescRank = append(
interestingResourceAttrsInDescRank, resourceHierarchy.Synonyms(attr)...,
)
}
interestingResourceAttrsInAscRank := interestingResourceAttrsInDescRank[:]
slices.Reverse(interestingResourceAttrsInAscRank)
return attribRankingStrategy{
interestingResourceAttrsInAscRank: interestingResourceAttrsInAscRank,
}
}
type attribRankingStrategy struct {
interestingResourceAttrsInAscRank []string
}
// The higher the score, the higher the rank
func (s *attribRankingStrategy) score(attrib v3.AttributeKey) int {
if attrib.Type == v3.AttributeKeyTypeResource {
// 3 + (-1) if attrib.Key is not an interesting resource attribute
return 3 + slices.Index(s.interestingResourceAttrsInAscRank, attrib.Key)
}
if attrib.Type == v3.AttributeKeyTypeTag {
return 1
}
return 0
}
func (s *attribRankingStrategy) sort(attribKeys []v3.AttributeKey) {
slices.SortFunc(attribKeys, func(a v3.AttributeKey, b v3.AttributeKey) int {
// To sort in descending order of score the return value must be negative when a > b
return s.score(b) - s.score(a)
})
}

View File

@ -138,6 +138,62 @@ func TestLogsFilterSuggestionsWithExistingFilter(t *testing.T) {
} }
} }
func TestResourceAttribsRankedHigherInLogsFilterSuggestions(t *testing.T) {
require := require.New(t)
tagKeys := []v3.AttributeKey{}
for _, k := range []string{"user_id", "user_email"} {
tagKeys = append(tagKeys, v3.AttributeKey{
Key: k,
Type: v3.AttributeKeyTypeTag,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
})
}
specialResourceAttrKeys := []v3.AttributeKey{}
for _, k := range []string{"service", "env"} {
specialResourceAttrKeys = append(specialResourceAttrKeys, v3.AttributeKey{
Key: k,
Type: v3.AttributeKeyTypeResource,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
})
}
otherResourceAttrKeys := []v3.AttributeKey{}
for _, k := range []string{"container_name", "container_id"} {
otherResourceAttrKeys = append(otherResourceAttrKeys, v3.AttributeKey{
Key: k,
Type: v3.AttributeKeyTypeResource,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
})
}
tb := NewFilterSuggestionsTestBed(t)
mockAttrKeysInDB := append(tagKeys, otherResourceAttrKeys...)
mockAttrKeysInDB = append(mockAttrKeysInDB, specialResourceAttrKeys...)
tb.mockAttribKeysQueryResponse(mockAttrKeysInDB)
expectedTopSuggestions := append(specialResourceAttrKeys, otherResourceAttrKeys...)
expectedTopSuggestions = append(expectedTopSuggestions, tagKeys...)
tb.mockAttribValuesQueryResponse(
expectedTopSuggestions[:2], [][]string{{"test"}, {"test"}},
)
suggestionsQueryParams := map[string]string{"examplesLimit": "2"}
suggestionsResp := tb.GetQBFilterSuggestionsForLogs(suggestionsQueryParams)
require.Equal(
expectedTopSuggestions,
suggestionsResp.AttributeKeys[:len(expectedTopSuggestions)],
)
}
// Mocks response for CH queries made by reader.GetLogAttributeKeys // Mocks response for CH queries made by reader.GetLogAttributeKeys
func (tb *FilterSuggestionsTestBed) mockAttribKeysQueryResponse( func (tb *FilterSuggestionsTestBed) mockAttribKeysQueryResponse(
attribsToReturn []v3.AttributeKey, attribsToReturn []v3.AttributeKey,