mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-05 16:00:41 +00:00
Compare commits
4 Commits
fix/body-p
...
nv/exec-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29a393e47e | ||
|
|
98f9850c53 | ||
|
|
c58f76f998 | ||
|
|
e39ad9e221 |
@@ -3,6 +3,7 @@ import { Tooltip } from 'antd';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ArrowDownLeft, ArrowUpRight, Calendar } from '@signozhq/icons';
|
||||
import { useAlertHistoryQueryParams } from 'pages/AlertDetails/hooks';
|
||||
import { AlertRuleStats } from 'types/api/alerts/def';
|
||||
import { calculateChange } from 'utils/calculateChange';
|
||||
|
||||
@@ -86,7 +87,7 @@ function StatsCard({
|
||||
}: StatsCardProps): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const relativeTime = urlQuery.get('relativeTime');
|
||||
const { relativeTime } = useAlertHistoryQueryParams();
|
||||
|
||||
const { changePercentage, changeDirection } = calculateChange(
|
||||
totalCurrentCount,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useGetAlertRuleDetailsTimelineGraphData } from 'pages/AlertDetails/hooks';
|
||||
import {
|
||||
useAlertHistoryQueryParams,
|
||||
useGetAlertRuleDetailsTimelineGraphData,
|
||||
} from 'pages/AlertDetails/hooks';
|
||||
import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateRenderer';
|
||||
import { AlertRuleTimelineGraphResponse } from 'types/api/alerts/def';
|
||||
|
||||
@@ -13,9 +15,7 @@ function GraphWrapper({
|
||||
}: {
|
||||
totalCurrentTriggers: number;
|
||||
}): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const relativeTime = urlQuery.get('relativeTime');
|
||||
const { relativeTime } = useAlertHistoryQueryParams();
|
||||
|
||||
const { isLoading, isRefetching, isError, data, isValidRuleId, ruleId } =
|
||||
useGetAlertRuleDetailsTimelineGraphData();
|
||||
|
||||
25
frontend/src/lib/getMinMax.test.ts
Normal file
25
frontend/src/lib/getMinMax.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import GetMinMax from './getMinMax';
|
||||
|
||||
const DAY_NS = 24 * 60 * 60 * 1000 * 1e6;
|
||||
|
||||
function rangeInDays(interval: string): number {
|
||||
const { minTime, maxTime } = GetMinMax(interval);
|
||||
return Math.round((maxTime - minTime) / DAY_NS);
|
||||
}
|
||||
|
||||
describe('GetMinMax', () => {
|
||||
it.each([
|
||||
['1month', 30],
|
||||
['2months', 60],
|
||||
['3months', 90],
|
||||
['6month', 180],
|
||||
['10d', 10],
|
||||
['3w', 21],
|
||||
])('resolves %p to %p days', (interval, days) => {
|
||||
expect(rangeInDays(interval)).toBe(days);
|
||||
});
|
||||
|
||||
it('throws on an unknown interval', () => {
|
||||
expect(() => GetMinMax('1y')).toThrow('invalid time type');
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ export const isValidShortHandDateTimeFormat = (time: string): boolean =>
|
||||
|
||||
const extractTimeAndUnit = (time: string): { time: number; unit: string } => {
|
||||
// Match the pattern
|
||||
const match = /^(\d+)([mhdw])$/.exec(time);
|
||||
const match = validCustomTimeRegex.exec(time);
|
||||
|
||||
if (match) {
|
||||
return { time: parseInt(match[1], 10), unit: match[2] };
|
||||
@@ -39,13 +39,16 @@ export const getMinTimeForRelativeTimes = (
|
||||
return getMinAgo({ minutes: 24 * 60 * time }).getTime();
|
||||
case 'w':
|
||||
return getMinAgo({ minutes: 24 * 60 * 7 * time }).getTime();
|
||||
case 'month':
|
||||
case 'months':
|
||||
return getMinAgo({ minutes: 24 * 60 * 30 * time }).getTime();
|
||||
default:
|
||||
return getMinAgo({ minutes: 1 }).getTime();
|
||||
}
|
||||
};
|
||||
|
||||
const GetMinMax = (
|
||||
interval: Time | string,
|
||||
interval: string,
|
||||
dateTimeRange?: [number, number],
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): GetMinMaxPayload => {
|
||||
|
||||
@@ -21,7 +21,8 @@ import { fromRuleDTOToPostableRuleV2 } from 'types/api/alerts/convert';
|
||||
|
||||
import AlertHeader from './AlertHeader/AlertHeader';
|
||||
import AlertNotFound from './AlertNotFound';
|
||||
import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
|
||||
import { useGetAlertRuleDetails } from './hooks';
|
||||
import { useRouteTabUtils } from './useRouteTabUtils';
|
||||
|
||||
import './AlertDetails.styles.scss';
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import { useAlertHistoryQueryParams } from '../hooks';
|
||||
|
||||
jest.mock('hooks/useUrlQuery', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const THIRTY_MINUTES_MS = 30 * 60 * 1000;
|
||||
|
||||
function renderWithSearch(
|
||||
search: string,
|
||||
): ReturnType<typeof useAlertHistoryQueryParams> {
|
||||
jest.mocked(useUrlQuery).mockReturnValue(new URLSearchParams(search));
|
||||
return renderHook(() => useAlertHistoryQueryParams()).result.current;
|
||||
}
|
||||
|
||||
describe('useAlertHistoryQueryParams', () => {
|
||||
it.each(['1y', '90s', 'now-1h', 'custom', 'null', ''])(
|
||||
'falls back to the default range for relativeTime=%p',
|
||||
(param) => {
|
||||
const { startTime, endTime, relativeTime } = renderWithSearch(
|
||||
`ruleId=1&relativeTime=${param}`,
|
||||
);
|
||||
|
||||
expect(endTime - startTime).toBeCloseTo(THIRTY_MINUTES_MS, -3);
|
||||
expect(relativeTime).toBe('30m');
|
||||
},
|
||||
);
|
||||
|
||||
it('uses a valid relativeTime', () => {
|
||||
const { startTime, endTime, relativeTime } = renderWithSearch(
|
||||
'ruleId=1&relativeTime=6h',
|
||||
);
|
||||
|
||||
expect(endTime - startTime).toBeCloseTo(6 * 60 * 60 * 1000, -3);
|
||||
expect(relativeTime).toBe('6h');
|
||||
});
|
||||
|
||||
it('prefers startTime and endTime over relativeTime', () => {
|
||||
const { startTime, endTime, relativeTime } = renderWithSearch(
|
||||
'ruleId=1&relativeTime=1y&startTime=1000&endTime=2000',
|
||||
);
|
||||
|
||||
expect({ startTime, endTime, relativeTime }).toStrictEqual({
|
||||
startTime: 1000,
|
||||
endTime: 2000,
|
||||
relativeTime: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,27 +29,21 @@ import {
|
||||
type RuletypesPostableRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertHistory from 'container/AlertHistory';
|
||||
import { TIMELINE_TABLE_PAGE_SIZE } from 'container/AlertHistory/constants';
|
||||
import {
|
||||
computeCursorForPage,
|
||||
useTimelineTableOrder,
|
||||
useTimelineTablePage,
|
||||
} from 'container/AlertHistory/Timeline/Table/useTimelineTableCursor';
|
||||
import { AlertDetailsTab, TimelineFilter } from 'container/AlertHistory/types';
|
||||
import { TimelineFilter } from 'container/AlertHistory/types';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { buildRoutePath } from 'lib/router/buildRoutePath';
|
||||
import GetMinMax, { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { History, Table } from '@signozhq/icons';
|
||||
import EditRules from 'pages/EditRules';
|
||||
import BetaTag from 'periscope/components/BetaTag/BetaTag';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { toPostableRuleDTOFromAlertDef } from 'types/api/alerts/convert';
|
||||
@@ -63,6 +57,7 @@ export const useAlertHistoryQueryParams = (): {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
hasStartAndEndParams: boolean;
|
||||
relativeTime: string | null;
|
||||
params: URLSearchParams;
|
||||
} => {
|
||||
const params = useUrlQuery();
|
||||
@@ -72,8 +67,9 @@ export const useAlertHistoryQueryParams = (): {
|
||||
const relativeTimeParam = params.get(QueryParams.relativeTime);
|
||||
|
||||
const relativeTime =
|
||||
(relativeTimeParam === 'null' ? null : relativeTimeParam) ||
|
||||
DEFAULT_TIME_RANGE;
|
||||
relativeTimeParam && isValidShortHandDateTimeFormat(relativeTimeParam)
|
||||
? relativeTimeParam
|
||||
: DEFAULT_TIME_RANGE;
|
||||
|
||||
const intStartTime = parseInt(startTime || '0', 10);
|
||||
const intEndTime = parseInt(endTime || '0', 10);
|
||||
@@ -93,63 +89,10 @@ export const useAlertHistoryQueryParams = (): {
|
||||
startTime: Math.floor(nanoToMilli(minTime)),
|
||||
endTime: Math.floor(nanoToMilli(maxTime)),
|
||||
hasStartAndEndParams,
|
||||
relativeTime: hasStartAndEndParams ? null : relativeTime,
|
||||
params,
|
||||
};
|
||||
};
|
||||
export const useRouteTabUtils = (): { routes: TabRoutes[] } => {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const getRouteUrl = (tab: AlertDetailsTab): string => {
|
||||
let route = '';
|
||||
let params = urlQuery.toString();
|
||||
const ruleIdKey = QueryParams.ruleId;
|
||||
const relativeTimeKey = QueryParams.relativeTime;
|
||||
|
||||
switch (tab) {
|
||||
case AlertDetailsTab.OVERVIEW:
|
||||
route = ROUTES.ALERT_OVERVIEW;
|
||||
break;
|
||||
case AlertDetailsTab.HISTORY:
|
||||
params = `${ruleIdKey}=${urlQuery.get(
|
||||
ruleIdKey,
|
||||
)}&${relativeTimeKey}=${urlQuery.get(relativeTimeKey)}`;
|
||||
route = ROUTES.ALERT_HISTORY;
|
||||
break;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${buildRoutePath(route)}?${params}`;
|
||||
};
|
||||
|
||||
const routes = [
|
||||
{
|
||||
Component: EditRules,
|
||||
name: (
|
||||
<div className="tab-item" data-testid="alert-details-tab-overview">
|
||||
<Table size={14} />
|
||||
Overview
|
||||
</div>
|
||||
),
|
||||
route: getRouteUrl(AlertDetailsTab.OVERVIEW),
|
||||
key: ROUTES.ALERT_OVERVIEW,
|
||||
},
|
||||
{
|
||||
Component: AlertHistory,
|
||||
name: (
|
||||
<div className="tab-item" data-testid="alert-details-tab-history">
|
||||
<History size={14} />
|
||||
History
|
||||
<BetaTag />
|
||||
</div>
|
||||
),
|
||||
route: getRouteUrl(AlertDetailsTab.HISTORY),
|
||||
key: ROUTES.ALERT_HISTORY,
|
||||
},
|
||||
];
|
||||
|
||||
return { routes };
|
||||
};
|
||||
type Props = {
|
||||
ruleId: string | null;
|
||||
isValidRuleId: boolean;
|
||||
@@ -356,12 +299,7 @@ export const useTimelineTable = ({
|
||||
nextCursor?: string;
|
||||
}): {
|
||||
paginationConfig: TablePaginationConfig;
|
||||
onChangeHandler: (
|
||||
pagination: TablePaginationConfig,
|
||||
sorter: any,
|
||||
filters: any,
|
||||
extra: any,
|
||||
) => void;
|
||||
onChangeHandler: TableProps<AlertRuleTimelineTableResponse>['onChange'];
|
||||
handleNextPage: () => void;
|
||||
handlePrevPage: () => void;
|
||||
hasNextPage: boolean;
|
||||
@@ -448,8 +386,11 @@ export const useAlertRuleStatusToggle = ({
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setAlertRuleState(data.data.state);
|
||||
invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
queryClient.refetchQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
|
||||
void invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
void queryClient.refetchQueries([
|
||||
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
|
||||
ruleId,
|
||||
]);
|
||||
notifications.success({
|
||||
message: `Alert has been ${
|
||||
data.data.state === 'disabled' ? 'disabled' : 'enabled'
|
||||
@@ -457,8 +398,11 @@ export const useAlertRuleStatusToggle = ({
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
queryClient.refetchQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
|
||||
void invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
void queryClient.refetchQueries([
|
||||
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
|
||||
ruleId,
|
||||
]);
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
|
||||
65
frontend/src/pages/AlertDetails/useRouteTabUtils.tsx
Normal file
65
frontend/src/pages/AlertDetails/useRouteTabUtils.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertHistory from 'container/AlertHistory';
|
||||
import { AlertDetailsTab } from 'container/AlertHistory/types';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { buildRoutePath } from 'lib/router/buildRoutePath';
|
||||
import { History, Table } from '@signozhq/icons';
|
||||
import EditRules from 'pages/EditRules';
|
||||
import BetaTag from 'periscope/components/BetaTag/BetaTag';
|
||||
|
||||
export const useRouteTabUtils = (): { routes: TabRoutes[] } => {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const getRouteUrl = (tab: AlertDetailsTab): string => {
|
||||
let route = '';
|
||||
let params = urlQuery.toString();
|
||||
const ruleIdKey = QueryParams.ruleId;
|
||||
const relativeTimeKey = QueryParams.relativeTime;
|
||||
|
||||
switch (tab) {
|
||||
case AlertDetailsTab.OVERVIEW:
|
||||
route = ROUTES.ALERT_OVERVIEW;
|
||||
break;
|
||||
case AlertDetailsTab.HISTORY:
|
||||
params = `${ruleIdKey}=${urlQuery.get(
|
||||
ruleIdKey,
|
||||
)}&${relativeTimeKey}=${urlQuery.get(relativeTimeKey)}`;
|
||||
route = ROUTES.ALERT_HISTORY;
|
||||
break;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${buildRoutePath(route)}?${params}`;
|
||||
};
|
||||
|
||||
const routes = [
|
||||
{
|
||||
Component: EditRules,
|
||||
name: (
|
||||
<div className="tab-item" data-testid="alert-details-tab-overview">
|
||||
<Table size={14} />
|
||||
Overview
|
||||
</div>
|
||||
),
|
||||
route: getRouteUrl(AlertDetailsTab.OVERVIEW),
|
||||
key: ROUTES.ALERT_OVERVIEW,
|
||||
},
|
||||
{
|
||||
Component: AlertHistory,
|
||||
name: (
|
||||
<div className="tab-item" data-testid="alert-details-tab-history">
|
||||
<History size={14} />
|
||||
History
|
||||
<BetaTag />
|
||||
</div>
|
||||
),
|
||||
route: getRouteUrl(AlertDetailsTab.HISTORY),
|
||||
key: ROUTES.ALERT_HISTORY,
|
||||
},
|
||||
];
|
||||
|
||||
return { routes };
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
package querybuilder_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/audittelemetryschema"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/logstelemetryschema"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/tracestelemetryschema"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// A key metadata does not report warns and the query runs, so in a select
|
||||
// field, group by, order by, or aggregation every storage's fallback must read.
|
||||
// The only failure allowed is invalid input for a context the signal does not
|
||||
// serve.
|
||||
func TestResolveColumn_UnknownKey(t *testing.T) {
|
||||
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
|
||||
start := uint64(releaseTime.Add(24 * time.Hour).UnixNano())
|
||||
end := start + uint64(time.Hour.Nanoseconds())
|
||||
|
||||
schemaMetadataStore := telemetrytypestest.NewMockSchemaMetadataStore(t)
|
||||
schemaMetadataStore.EXPECT().GetBaseColumnEvolutions(mock.Anything, telemetrytypes.SignalTraces, telemetrytypes.FieldContextAttribute).Return(tracestelemetryschema.MockAttributeEvolutionData(releaseTime), nil).Maybe()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
storage qbtypes.Storage
|
||||
query qbtypes.QueryInfo
|
||||
}{
|
||||
{
|
||||
name: "Logs",
|
||||
storage: logstelemetryschema.NewStorage(),
|
||||
query: qbtypes.QueryInfo{Signal: telemetrytypes.SignalLogs, StartNs: start, EndNs: end},
|
||||
},
|
||||
{
|
||||
name: "Logs_JSONBody",
|
||||
storage: logstelemetryschema.NewStorage(),
|
||||
query: qbtypes.QueryInfo{Signal: telemetrytypes.SignalLogs, StartNs: start, EndNs: end, BodyJSONOn: true},
|
||||
},
|
||||
{
|
||||
name: "Traces",
|
||||
storage: tracestelemetryschema.NewStorage(schemaMetadataStore),
|
||||
query: qbtypes.QueryInfo{Signal: telemetrytypes.SignalTraces, StartNs: start, EndNs: end},
|
||||
},
|
||||
{
|
||||
name: "Traces_JSONAttributes",
|
||||
storage: tracestelemetryschema.NewStorage(schemaMetadataStore),
|
||||
query: qbtypes.QueryInfo{Signal: telemetrytypes.SignalTraces, StartNs: start, EndNs: end, TraceAttrsJSONOn: true},
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
storage: metricstelemetryschema.NewStorage(),
|
||||
query: qbtypes.QueryInfo{Signal: telemetrytypes.SignalMetrics, StartNs: start, EndNs: end},
|
||||
},
|
||||
{
|
||||
name: "Audit",
|
||||
storage: audittelemetryschema.NewStorage(),
|
||||
query: qbtypes.QueryInfo{Signal: telemetrytypes.SignalLogs, StartNs: start, EndNs: end},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
for _, value := range (telemetrytypes.FieldContext{}).Enum() {
|
||||
fieldContext, ok := value.(telemetrytypes.FieldContext)
|
||||
require.True(t, ok)
|
||||
|
||||
t.Run(testCase.name+"_"+fieldContext.StringValue(), func(t *testing.T) {
|
||||
key := telemetrytypes.TelemetryFieldKey{Name: "unknown.key", FieldContext: fieldContext, Signal: testCase.query.Signal}
|
||||
|
||||
_, err := querybuilder.ResolveColumn(context.Background(), testCase.query, testCase.storage, &key, telemetrytypes.FieldDataTypeUnspecified, map[string][]*telemetrytypes.TelemetryFieldKey{})
|
||||
if err != nil {
|
||||
assert.True(t, errors.Ast(err, errors.TypeInvalidInput), "%v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,7 +505,7 @@ func (m *storage) Fallback(_ context.Context, _ qbtypes.QueryInfo, key *telemetr
|
||||
var keys []*telemetrytypes.TelemetryFieldKey
|
||||
switch key.FieldContext {
|
||||
case telemetrytypes.FieldContextBody:
|
||||
keys = []*telemetrytypes.TelemetryFieldKey{bodyPath(key.Name, key.FieldDataType, value)}
|
||||
keys = []*telemetrytypes.TelemetryFieldKey{telemetrytypes.NewTelemetryFieldKey(key.Name, key.FieldContext, key.FieldDataType)}
|
||||
case telemetrytypes.FieldContextUnspecified:
|
||||
if column, ok := logsV2Columns[key.Name]; ok {
|
||||
keys = []*telemetrytypes.TelemetryFieldKey{{Name: key.Name, FieldContext: telemetrytypes.FieldContextLog, FieldDataType: querybuilder.ColumnDataType(column)}}
|
||||
|
||||
@@ -240,14 +240,12 @@ def test_json_body_unknown_key_aggregation_over_body(
|
||||
assert tuple(data[0]) == (6, 4), data[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", ["", "body."], ids=["bare", "body_prefix"])
|
||||
def test_json_body_unknown_key_body_path_parity(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
export_json_types: Callable[[list[JSONPathType]], None],
|
||||
prefix: str,
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
@@ -256,11 +254,10 @@ def test_json_body_unknown_key_body_path_parity(
|
||||
(distributed_field_keys); unknown_field is left unregistered.
|
||||
|
||||
Tests:
|
||||
Grouping, counting distinct values, and selecting by the registered body path
|
||||
(known_field, metadata-driven) and by the unregistered one (unknown_field,
|
||||
fallback body path) return identical results, for the bare key and the
|
||||
body.-prefixed key — the body counterpart of the attribute parity in
|
||||
13_unknown_key_collisions.py.
|
||||
Grouping/filtering by the registered body path (known_field, metadata-driven) and
|
||||
by the unregistered one (unknown_field, synthesized body branch) return identical
|
||||
results — the synthesize fallback reproduces exported-metadata resolution for body
|
||||
paths, the body counterpart of the attribute parity in 13_unknown_key_collisions.py.
|
||||
"""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
logs = []
|
||||
@@ -282,10 +279,7 @@ def test_json_body_unknown_key_body_path_parity(
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
counts: dict[str, dict[str, int]] = {}
|
||||
distinct: dict[str, int] = {}
|
||||
selected: dict[str, list[str]] = {}
|
||||
for key in ("known_field", "unknown_field"):
|
||||
field = f"{prefix}{key}"
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -295,42 +289,13 @@ def test_json_body_unknown_key_body_path_parity(
|
||||
"A",
|
||||
"logs",
|
||||
[build_aggregation("count()")],
|
||||
group_by=[build_group_by_field(field, field_data_type="", field_context="")],
|
||||
group_by=[build_group_by_field(key, field_data_type="", field_context="")],
|
||||
filter_expression=f'service.name = "{SERVICE}"',
|
||||
)
|
||||
],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, f"{field}: {response.text}"
|
||||
assert response.status_code == HTTPStatus.OK, f"{key}: {response.text}"
|
||||
counts[key] = {row[0]: row[-1] for row in get_scalar_table_data(response.json())}
|
||||
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
now,
|
||||
[
|
||||
build_scalar_query(
|
||||
"A",
|
||||
"logs",
|
||||
[build_aggregation(f"count_distinct({field})")],
|
||||
filter_expression=f'service.name = "{SERVICE}"',
|
||||
)
|
||||
],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, f"{field}: {response.text}"
|
||||
distinct[key] = get_scalar_table_data(response.json())[0][-1]
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
request_type=RequestType.RAW,
|
||||
queries=[build_raw_query("A", "logs", limit=100, filter_expression=f'service.name = "{SERVICE}"', select_fields=[{"name": field}])],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, f"{field}: {response.text}"
|
||||
selected[key] = sorted(row["data"][key] for row in get_rows(response))
|
||||
|
||||
assert counts["known_field"] == counts["unknown_field"], f"registered {counts['known_field']} != synthesized {counts['unknown_field']}"
|
||||
assert counts["known_field"] == {"alpha": 2, "beta": 1}, counts["known_field"]
|
||||
assert distinct == {"known_field": 2, "unknown_field": 2}, distinct
|
||||
assert selected == {"known_field": ["alpha", "alpha", "beta"], "unknown_field": ["alpha", "alpha", "beta"]}, selected
|
||||
|
||||
@@ -442,3 +442,51 @@ def test_builder_refreshing_a_sliding_time_range(
|
||||
assert uncached.status_code == HTTPStatus.OK, uncached.text
|
||||
|
||||
assert_results_equal(from_cache.json(), uncached.json(), "A", "the twentieth refresh")
|
||||
|
||||
|
||||
def test_builder_rows_scanned_when_refreshing(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
metric_name = f"cache_rows_scanned_{uuid4().hex[:8]}"
|
||||
|
||||
# 90 minutes back so even the last refresh closes clear of the flux
|
||||
# interval, which holds recent data out of the cache
|
||||
start_time = datetime.fromtimestamp(int((datetime.now(tz=UTC) - timedelta(minutes=90)).timestamp()) // 60 * 60, tz=UTC)
|
||||
start_time_ms = int(start_time.timestamp() * 1000)
|
||||
|
||||
query = [build_builder_query("A", metric_name, "max", "max")]
|
||||
|
||||
insert_metrics(
|
||||
[
|
||||
Metrics(
|
||||
metric_name=metric_name,
|
||||
labels={"service": "api"},
|
||||
timestamp=start_time + timedelta(minutes=minute),
|
||||
value=minute,
|
||||
type_="Gauge",
|
||||
is_monotonic=False,
|
||||
)
|
||||
for minute in range(70)
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# a dashboard left open on a one hour range, re-running a minute later each
|
||||
# time. rowsScanned is what answering the request cost, so a refresh served
|
||||
# mostly out of the cache cannot report more than reading its range afresh
|
||||
for refresh in range(10):
|
||||
refresh_start_ms = start_time_ms + refresh * MINUTE_MS
|
||||
from_cache = make_query_request(signoz, token, refresh_start_ms, refresh_start_ms + 60 * MINUTE_MS, query, no_cache=False)
|
||||
assert from_cache.status_code == HTTPStatus.OK, from_cache.text
|
||||
|
||||
uncached = make_query_request(signoz, token, refresh_start_ms, refresh_start_ms + 60 * MINUTE_MS, query, no_cache=True)
|
||||
assert uncached.status_code == HTTPStatus.OK, uncached.text
|
||||
|
||||
rows_scanned_from_cache = from_cache.json()["data"]["meta"]["rowsScanned"]
|
||||
rows_scanned_uncached = uncached.json()["data"]["meta"]["rowsScanned"]
|
||||
assert rows_scanned_uncached > 0, f"refresh {refresh}"
|
||||
assert rows_scanned_from_cache <= rows_scanned_uncached, f"refresh {refresh}: {rows_scanned_from_cache} rows scanned when served from the cache, {rows_scanned_uncached} when read afresh"
|
||||
|
||||
Reference in New Issue
Block a user