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 {
height: calc(100% - 30px);
${({ $panelType }): StyledCSS =>
$panelType === PANEL_TYPES.TABLE
? css`
height: 100%;
`
: css`
height: calc(100% - 30px);
`}
padding: 0;
}
`;

View File

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

View File

@ -43,6 +43,15 @@
.ant-select-item {
display: flex;
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 {
@ -56,28 +65,25 @@
}
.dropdown-value {
display: flex;
justify-content: space-between;
align-items: center;
display: grid;
grid-template-columns: 1fr max-content;
.option-text {
max-width: 180px;
padding: 0 8px;
}
.toggle-tag-label {
padding-left: 8px;
right: 40px;
font-weight: normal;
position: absolute;
font-weight: 500;
}
}
}
}
.dropdown-styles {
min-width: 300px;
max-width: 350px;
min-width: 400px;
max-width: 500px;
}
.lightMode {

View File

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

View File

@ -1,3 +1,4 @@
import { Color } from '@signozhq/design-tokens';
import { Select } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
// ** Constants
@ -34,6 +35,7 @@ export function HavingFilter({
const [currentFormValue, setCurrentFormValue] = useState<HavingForm>(
initialHavingValues,
);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { isMulti } = useTagValidation(
currentFormValue.op,
@ -198,6 +200,29 @@ export function HavingFilter({
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(() => {
parseSearchText(searchText);
}, [searchText, parseSearchText]);
@ -209,28 +234,36 @@ export function HavingFilter({
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
return (
<Select
getPopupContainer={popupContainer}
autoClearSearchValue={false}
mode="multiple"
onSearch={handleSearch}
searchValue={searchText}
tagRender={tagRender}
value={localValues}
data-testid="havingSelect"
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={{ width: '100%' }}
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
placeholder="GroupBy(operation) > 5"
onDeselect={handleDeselect}
onChange={handleChange}
onSelect={handleSelect}
>
{options.map((opt) => (
<Select.Option key={opt.value} value={opt.value} title="havingOption">
{opt.label}
</Select.Option>
))}
</Select>
<>
<Select
getPopupContainer={popupContainer}
autoClearSearchValue={false}
mode="multiple"
onSearch={handleSearch}
searchValue={searchText}
tagRender={tagRender}
value={localValues}
data-testid="havingSelect"
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={{ width: '100%' }}
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
placeholder="GroupBy(operation) > 5"
onDeselect={handleDeselect}
onChange={handleChange}
onSelect={handleSelect}
onFocus={handleFocus}
onBlur={handleBlur}
status={errorMessage ? 'error' : undefined}
>
{options.map((opt) => (
<Select.Option key={opt.value} value={opt.value} title="havingOption">
{opt.label}
</Select.Option>
))}
</Select>
{errorMessage && (
<div style={{ color: Color.BG_CHERRY_500 }}>{errorMessage}</div>
)}
</>
);
}

View File

@ -286,16 +286,62 @@ function QueryBuilderSearchV2(
parsedValue = value;
}
if (currentState === DropdownState.ATTRIBUTE_KEY) {
setCurrentFilterItem((prev) => ({
...prev,
key: parsedValue as BaseAutocompleteData,
op: '',
value: '',
}));
setCurrentState(DropdownState.OPERATOR);
setSearchValue((parsedValue as BaseAutocompleteData)?.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) => ({
...prev,
key: parsedValue as BaseAutocompleteData,
op: '',
value: '',
}));
setCurrentState(DropdownState.OPERATOR);
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
}
} 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) => [
...prev,
{
@ -399,6 +445,7 @@ function QueryBuilderSearchV2(
whereClauseConfig?.customKey === 'body' &&
whereClauseConfig?.customOp === OPERATORS.CONTAINS
) {
// eslint-disable-next-line sonarjs/no-identical-functions
setTags((prev) => [
...prev,
{
@ -519,19 +566,20 @@ function QueryBuilderSearchV2(
setCurrentState(DropdownState.OPERATOR);
}
}
if (suggestionsData?.payload?.attributes?.length === 0) {
// again let's not auto select anything for the user
if (tagOperator) {
setCurrentFilterItem({
key: {
key: tagKey.split(' ')[0],
key: tagKey,
dataType: DataTypes.EMPTY,
type: '',
isColumn: false,
isJSON: false,
},
op: '',
op: tagOperator,
value: '',
});
setCurrentState(DropdownState.OPERATOR);
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
}
} else if (
// 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
useEffect(() => {
if (currentState === DropdownState.ATTRIBUTE_KEY) {
const { tagKey } = getTagToken(searchValue);
if (isLogsExplorerPage) {
setDropdownOptions(
suggestionsData?.payload?.attributes?.map((key) => ({
// add the user typed option in the dropdown to select that and move ahead irrespective of the matches and all
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,
value: key,
})) || [],
);
})) || []),
]);
} else {
setDropdownOptions(
data?.payload?.attributeKeys?.map((key) => ({
@ -643,12 +710,14 @@ function QueryBuilderSearchV2(
op.label.startsWith(partialOperator.toLocaleUpperCase()),
);
}
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
label: operator,
value: operator,
}));
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
} else {
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
@ -663,6 +732,7 @@ function QueryBuilderSearchV2(
op.label.startsWith(partialOperator.toLocaleUpperCase()),
);
}
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
}
}

View File

@ -52,6 +52,8 @@
height: 40px;
justify-content: end;
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/DATA-DOG/go-sqlmock v1.5.2
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_sync v0.0.0-20230822164844-1b861a431974
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/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.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/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
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"
"strings"
"github.com/SigNoz/signoz-otel-collector/exporter/clickhouselogsexporter/logsv2"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.uber.org/zap"
@ -36,26 +37,7 @@ func (r *ClickHouseReader) GetQBFilterSuggestionsForLogs(
suggestions.AttributeKeys = attribKeysResp.AttributeKeys
// Rank suggested attributes
slices.SortFunc(suggestions.AttributeKeys, func(a v3.AttributeKey, b v3.AttributeKey) int {
// 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)
})
attribRanker.sort(suggestions.AttributeKeys)
// Put together suggested example queries.
@ -268,3 +250,59 @@ func (r *ClickHouseReader) getValuesForLogAttributes(
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
func (tb *FilterSuggestionsTestBed) mockAttribKeysQueryResponse(
attribsToReturn []v3.AttributeKey,