mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-16 11:20:31 +01:00
Compare commits
3 Commits
issue-1000
...
issue_5674
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a59e372f07 | ||
|
|
ca368a5b38 | ||
|
|
dab5ceee0d |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -53,7 +53,6 @@ jobs:
|
||||
- queriermetrics
|
||||
- querierscalar
|
||||
- queriercommon
|
||||
- queriervariables
|
||||
- rawexportdata
|
||||
- role
|
||||
- rootuser
|
||||
|
||||
@@ -1495,6 +1495,7 @@ components:
|
||||
- cassandradb
|
||||
- redis
|
||||
- cloudsql_postgres
|
||||
- memorystore_redis
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
@@ -4259,34 +4260,6 @@ components:
|
||||
type: number
|
||||
clusterName:
|
||||
type: string
|
||||
counts:
|
||||
properties:
|
||||
daemonSets:
|
||||
format: int64
|
||||
type: integer
|
||||
deployments:
|
||||
format: int64
|
||||
type: integer
|
||||
jobs:
|
||||
format: int64
|
||||
type: integer
|
||||
namespaces:
|
||||
format: int64
|
||||
type: integer
|
||||
nodes:
|
||||
format: int64
|
||||
type: integer
|
||||
statefulSets:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- nodes
|
||||
- namespaces
|
||||
- deployments
|
||||
- daemonSets
|
||||
- jobs
|
||||
- statefulSets
|
||||
type: object
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -4307,7 +4280,6 @@ components:
|
||||
- nodeCountsByReadiness
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- counts
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesClusters:
|
||||
@@ -4829,26 +4801,6 @@ components:
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
counts:
|
||||
properties:
|
||||
daemonSets:
|
||||
format: int64
|
||||
type: integer
|
||||
deployments:
|
||||
format: int64
|
||||
type: integer
|
||||
jobs:
|
||||
format: int64
|
||||
type: integer
|
||||
statefulSets:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- deployments
|
||||
- daemonSets
|
||||
- jobs
|
||||
- statefulSets
|
||||
type: object
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -4872,7 +4824,6 @@ components:
|
||||
- namespaceMemory
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- counts
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesNamespaces:
|
||||
|
||||
@@ -2814,6 +2814,7 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
cassandradb = 'cassandradb',
|
||||
redis = 'redis',
|
||||
cloudsql_postgres = 'cloudsql_postgres',
|
||||
memorystore_redis = 'memorystore_redis',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
@@ -5669,39 +5670,6 @@ export interface InframonitoringtypesChecksDTO {
|
||||
type: InframonitoringtypesCheckTypeDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
daemonSets: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
deployments: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
jobs: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
namespaces: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
nodes: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
statefulSets: number;
|
||||
};
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -5846,10 +5814,6 @@ export interface InframonitoringtypesClusterRecordDTO {
|
||||
* @type string
|
||||
*/
|
||||
clusterName: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
counts: InframonitoringtypesClusterRecordDTOCounts;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
@@ -6405,29 +6369,6 @@ export interface InframonitoringtypesJobsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
daemonSets: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
deployments: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
jobs: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
statefulSets: number;
|
||||
};
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6439,10 +6380,6 @@ export type InframonitoringtypesNamespaceRecordDTOMeta =
|
||||
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
counts: InframonitoringtypesNamespaceRecordDTOCounts;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
|
||||
@@ -89,20 +89,21 @@ describe('UnpricedModelsTab (integration)', () => {
|
||||
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
|
||||
await selectRule(user, MODEL, 'rule-openai');
|
||||
|
||||
const trigger = screen.getByTestId(`map-to-select-${MODEL}`);
|
||||
expect(
|
||||
within(screen.getByTestId(`map-to-select-${MODEL}`)).getByText(
|
||||
'openai:gpt-4o ($3.00/$9.00)',
|
||||
),
|
||||
within(trigger).getByText('openai:gpt-4o ($3.00/$9.00)'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(await screen.findByTestId('unpriced-map-cancel-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
const trigger = screen.getByTestId(`map-to-select-${MODEL}`);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
within(trigger).getByText('Select / Create a pricing model'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
within(trigger).queryByText('openai:gpt-4o ($3.00/$9.00)'),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(
|
||||
within(trigger).getByText('Select / Create a pricing model'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('commits the mapping in one request when confirmed', async () => {
|
||||
|
||||
@@ -191,6 +191,14 @@
|
||||
min-height: 0;
|
||||
overflow-y: visible;
|
||||
|
||||
.time-series-view-container-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-series-view {
|
||||
flex-shrink: 0;
|
||||
height: 65vh;
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
getListQuery,
|
||||
getQueryByPanelType,
|
||||
} from 'container/LogsExplorerViews/explorerUtils';
|
||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
@@ -460,17 +461,18 @@ function LogsExplorerViewsContainer({
|
||||
)}
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
|
||||
<div className="time-series-view-container">
|
||||
<div className="time-series-view-container-header">
|
||||
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
|
||||
</div>
|
||||
<TimeSeriesView
|
||||
isLoading={isLoading || isFetching}
|
||||
data={data}
|
||||
isError={isError}
|
||||
error={error as APIError}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onYAxisUnitChange={onUnitChange}
|
||||
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
|
||||
dataSource={DataSource.LOGS}
|
||||
setWarning={setWarning}
|
||||
allowExport
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -311,7 +311,6 @@ function TimeSeries({
|
||||
dataSource={DataSource.METRICS}
|
||||
error={queries[index].error as APIError}
|
||||
setWarning={setWarning}
|
||||
allowExport
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,6 @@
|
||||
min-height: 350px;
|
||||
padding: 0px 12px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: 50vh;
|
||||
min-height: 350px;
|
||||
|
||||
@@ -16,7 +16,6 @@ import Uplot from 'components/Uplot';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
|
||||
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch';
|
||||
@@ -42,14 +41,11 @@ import { SuccessResponse, Warning } from 'types/api';
|
||||
import { LegendPosition } from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryRangeResponseV5 } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import TimeseriesExportMenu from './TimeseriesExportMenu';
|
||||
|
||||
import './TimeSeriesView.styles.scss';
|
||||
|
||||
function TimeSeriesView({
|
||||
@@ -63,8 +59,6 @@ function TimeSeriesView({
|
||||
setWarning,
|
||||
panelType = PANEL_TYPES.TIME_SERIES,
|
||||
stackBarChart = false,
|
||||
allowExport = false,
|
||||
onYAxisUnitChange,
|
||||
}: TimeSeriesViewProps): JSX.Element {
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -250,33 +244,10 @@ function TimeSeriesView({
|
||||
[baseChartOptions, stackedBands],
|
||||
);
|
||||
|
||||
const showExport = allowExport && !!data?.rawV5Response;
|
||||
const showHeader = showExport || !!onYAxisUnitChange;
|
||||
|
||||
return (
|
||||
<div className="time-series-view">
|
||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||
|
||||
{showHeader && (
|
||||
<div className="time-series-view__header">
|
||||
<div>
|
||||
{onYAxisUnitChange && (
|
||||
<BuilderUnitsFilter onChange={onYAxisUnitChange} yAxisUnit={yAxisUnit} />
|
||||
)}
|
||||
</div>
|
||||
{showExport && data?.rawV5Response && (
|
||||
<TimeseriesExportMenu
|
||||
dataSource={dataSource}
|
||||
yAxisUnit={yAxisUnit}
|
||||
queryResponse={data.rawV5Response}
|
||||
query={currentQuery}
|
||||
legendMap={data.legendMap}
|
||||
fileName={`${dataSource}-timeseries`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="graph-container"
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
@@ -324,11 +295,7 @@ function TimeSeriesView({
|
||||
}
|
||||
|
||||
interface TimeSeriesViewProps {
|
||||
data?: SuccessResponse<MetricRangePayloadProps> & {
|
||||
warning?: Warning;
|
||||
rawV5Response?: QueryRangeResponseV5;
|
||||
legendMap?: Record<string, string>;
|
||||
};
|
||||
data?: SuccessResponse<MetricRangePayloadProps> & { warning?: Warning };
|
||||
yAxisUnit?: string;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
@@ -338,11 +305,6 @@ interface TimeSeriesViewProps {
|
||||
setWarning?: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
panelType?: PANEL_TYPES;
|
||||
stackBarChart?: boolean;
|
||||
// Opt-in: render the client-side export menu (Logs explorer for now).
|
||||
allowExport?: boolean;
|
||||
// Opt-in: render the y-axis unit selector in the header (views without their
|
||||
// own selector, e.g. Logs). Metrics keeps its separate YAxisUnitSelector.
|
||||
onYAxisUnitChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
TimeSeriesView.defaultProps = {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
.timeseries-export-popover {
|
||||
width: 240px;
|
||||
padding: 0 12px 12px 12px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--periscope-font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.export-format {
|
||||
padding: 12px 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// radio option labels — radix popover content inherits the root font
|
||||
// size; pin to the app's 13px base the antd popover used to impose
|
||||
label {
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
.export-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Download } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
|
||||
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useClientExport } from 'hooks/useExportData/useClientExport';
|
||||
import { ExportFormat } from 'lib/exportData/types';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryRangeResponseV5 } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import './TimeseriesExportMenu.styles.scss';
|
||||
|
||||
interface TimeseriesExportMenuProps {
|
||||
dataSource: DataSource;
|
||||
queryResponse: QueryRangeResponseV5;
|
||||
query?: Query;
|
||||
yAxisUnit?: string;
|
||||
legendMap?: Record<string, string>;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
// Download menu for in-memory timeseries data (client-side serialization).
|
||||
// The raw/list backend export keeps its own menu in DownloadOptionsMenu.
|
||||
export default function TimeseriesExportMenu({
|
||||
dataSource,
|
||||
queryResponse,
|
||||
query,
|
||||
yAxisUnit,
|
||||
legendMap,
|
||||
fileName,
|
||||
}: TimeseriesExportMenuProps): JSX.Element {
|
||||
const [exportFormat, setExportFormat] = useState<string>(ExportFormat.Csv);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const { isExporting, handleExport: handleClientExport } = useClientExport({
|
||||
response: queryResponse,
|
||||
query,
|
||||
yAxisUnit,
|
||||
legendMap,
|
||||
fileName,
|
||||
});
|
||||
|
||||
const handleExport = useCallback((): void => {
|
||||
setIsPopoverOpen(false);
|
||||
handleClientExport({ format: exportFormat as ExportFormat });
|
||||
}, [exportFormat, handleClientExport]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<TooltipSimple title="Download">
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
aria-label="Download"
|
||||
data-testid={`timeseries-export-${dataSource}`}
|
||||
disabled={isExporting}
|
||||
loading={isExporting}
|
||||
>
|
||||
<Download size={14} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipSimple>
|
||||
<PopoverContent align="end" className="timeseries-export-popover">
|
||||
<div className="export-format">
|
||||
<Typography.Text className="title">FORMAT</Typography.Text>
|
||||
<RadioGroup value={exportFormat} onChange={setExportFormat}>
|
||||
<RadioGroupItem value={ExportFormat.Csv}>csv</RadioGroupItem>
|
||||
<RadioGroupItem value={ExportFormat.Jsonl}>jsonl</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className="export-button"
|
||||
onClick={handleExport}
|
||||
disabled={isExporting}
|
||||
loading={isExporting}
|
||||
prefix={<Download size={16} />}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import store from 'store';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import TimeSeriesView from '../TimeSeriesView';
|
||||
|
||||
jest.mock('components/Uplot', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="uplot-chart" />,
|
||||
}));
|
||||
|
||||
jest.mock('../TimeseriesExportMenu', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="timeseries-export-menu" />,
|
||||
}));
|
||||
|
||||
jest.mock('container/QueryBuilder/filters/BuilderUnitsFilter', () => ({
|
||||
BuilderUnitsFilter: (): JSX.Element => (
|
||||
<div data-testid="builder-units-filter" />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: (): unknown => ({ currentQuery: null }),
|
||||
}));
|
||||
|
||||
jest.mock('lib/uPlotLib/getUplotChartOptions', () => ({
|
||||
getUPlotChartOptions: (): unknown => ({}),
|
||||
}));
|
||||
|
||||
jest.mock('lib/uPlotLib/utils/getUplotChartData', () => ({
|
||||
getUPlotChartData: (): number[][] => [
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
],
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils',
|
||||
() => ({ stackSeries: (): unknown => ({ data: [], bands: [] }) }),
|
||||
);
|
||||
|
||||
jest.mock('container/GridCardLayout/GridCard/utils', () => ({
|
||||
getLocalStorageGraphVisibilityState: (): unknown => ({
|
||||
graphVisibilityStates: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Timezone', () => ({
|
||||
useTimezone: (): unknown => ({ timezone: { value: 'UTC' } }),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDimensions', () => ({
|
||||
useResizeObserver: (): unknown => ({ width: 800, height: 400 }),
|
||||
}));
|
||||
|
||||
jest.mock('api/common/logEvent', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockStore = configureStore([])({ ...store.getState() });
|
||||
|
||||
const rawV5Response = {
|
||||
type: 'time_series',
|
||||
data: { results: [] },
|
||||
meta: {},
|
||||
};
|
||||
|
||||
function makeData(withRawV5: boolean): any {
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: { data: { result: [], resultType: '' } },
|
||||
...(withRawV5 ? { rawV5Response, legendMap: {} } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function renderView(props: {
|
||||
allowExport?: boolean;
|
||||
withRawV5?: boolean;
|
||||
onYAxisUnitChange?: (value: string) => void;
|
||||
}): ReturnType<typeof render> {
|
||||
const { allowExport, withRawV5 = true, onYAxisUnitChange } = props;
|
||||
return render(
|
||||
<Provider store={mockStore}>
|
||||
<MemoryRouter>
|
||||
<TimeSeriesView
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
isFilterApplied
|
||||
dataSource={DataSource.LOGS}
|
||||
data={makeData(withRawV5)}
|
||||
allowExport={allowExport}
|
||||
onYAxisUnitChange={onYAxisUnitChange}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('TimeSeriesView header gating', () => {
|
||||
it('renders the export menu when allowExport is set and raw V5 data is present', () => {
|
||||
const { queryByTestId } = renderView({ allowExport: true });
|
||||
expect(queryByTestId('timeseries-export-menu')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no export menu without allowExport', () => {
|
||||
const { queryByTestId } = renderView({});
|
||||
expect(queryByTestId('timeseries-export-menu')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no export menu when the raw V5 response is missing', () => {
|
||||
const { queryByTestId } = renderView({ allowExport: true, withRawV5: false });
|
||||
expect(queryByTestId('timeseries-export-menu')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the unit selector only when onYAxisUnitChange is passed', () => {
|
||||
const withUnit = renderView({ onYAxisUnitChange: jest.fn() });
|
||||
expect(withUnit.queryByTestId('builder-units-filter')).toBeInTheDocument();
|
||||
withUnit.unmount();
|
||||
|
||||
const withoutUnit = renderView({ allowExport: true });
|
||||
expect(
|
||||
withoutUnit.queryByTestId('builder-units-filter'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no header row when neither export nor unit selector is enabled', () => {
|
||||
const { container } = renderView({ withRawV5: false });
|
||||
expect(container.querySelector('.time-series-view__header')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { QueryRangeResponseV5 } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import TimeseriesExportMenu from '../TimeseriesExportMenu';
|
||||
|
||||
const mockHandleExport = jest.fn();
|
||||
let mockIsExporting = false;
|
||||
|
||||
jest.mock('hooks/useExportData/useClientExport', () => ({
|
||||
useClientExport: (): unknown => ({
|
||||
isExporting: mockIsExporting,
|
||||
handleExport: mockHandleExport,
|
||||
}),
|
||||
}));
|
||||
|
||||
const response = {
|
||||
type: 'time_series',
|
||||
data: { results: [] },
|
||||
meta: {},
|
||||
} as unknown as QueryRangeResponseV5;
|
||||
|
||||
const TEST_ID = `timeseries-export-${DataSource.LOGS}`;
|
||||
|
||||
function renderMenu(): void {
|
||||
render(
|
||||
<TimeseriesExportMenu
|
||||
dataSource={DataSource.LOGS}
|
||||
queryResponse={response}
|
||||
fileName="logs-timeseries"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('TimeseriesExportMenu', () => {
|
||||
beforeEach(() => {
|
||||
mockHandleExport.mockReset();
|
||||
mockIsExporting = false;
|
||||
});
|
||||
|
||||
it('renders the download trigger button', () => {
|
||||
renderMenu();
|
||||
expect(screen.getByTestId(TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows only format options — no shape, row-count, or column controls', () => {
|
||||
renderMenu();
|
||||
fireEvent.click(screen.getByTestId(TEST_ID));
|
||||
|
||||
expect(screen.getByText('FORMAT')).toBeInTheDocument();
|
||||
expect(screen.getByRole('radio', { name: 'csv' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('radio', { name: 'jsonl' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Number of Rows')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Columns')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('radio', { name: 'long' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('radio', { name: 'wide' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('exports as csv by default', () => {
|
||||
renderMenu();
|
||||
fireEvent.click(screen.getByTestId(TEST_ID));
|
||||
fireEvent.click(screen.getByText('Export'));
|
||||
|
||||
expect(mockHandleExport).toHaveBeenCalledTimes(1);
|
||||
expect(mockHandleExport).toHaveBeenCalledWith({ format: 'csv' });
|
||||
});
|
||||
|
||||
it('exports as jsonl when selected', () => {
|
||||
renderMenu();
|
||||
fireEvent.click(screen.getByTestId(TEST_ID));
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'jsonl' }));
|
||||
fireEvent.click(screen.getByText('Export'));
|
||||
|
||||
expect(mockHandleExport).toHaveBeenCalledWith({ format: 'jsonl' });
|
||||
});
|
||||
|
||||
it('disables the trigger while an export is in progress', () => {
|
||||
mockIsExporting = true;
|
||||
renderMenu();
|
||||
|
||||
expect(screen.getByTestId(TEST_ID)).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -1,41 +1,10 @@
|
||||
import { SuccessResponse } from 'types/api/index';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryRangeResponseV5, TimeSeriesData } from 'types/api/v5/queryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
type ConvertibleData = SuccessResponse<MetricRangePayloadProps> & {
|
||||
rawV5Response?: QueryRangeResponseV5;
|
||||
};
|
||||
|
||||
// Applies the same ns→ms conversion to the raw V5 tree, so client-side export
|
||||
// serializes the values the chart displays (not the original nanoseconds).
|
||||
function convertRawV5ValuesToMs(
|
||||
response: QueryRangeResponseV5,
|
||||
): QueryRangeResponseV5 {
|
||||
if (response.type !== 'time_series') {
|
||||
return response;
|
||||
}
|
||||
|
||||
const results = (response.data.results as TimeSeriesData[]).map((result) => ({
|
||||
...result,
|
||||
aggregations: (result.aggregations ?? []).map((bucket) => ({
|
||||
...bucket,
|
||||
series: (bucket.series ?? []).map((series) => ({
|
||||
...series,
|
||||
values: (series.values ?? []).map((value) => ({
|
||||
...value,
|
||||
value: value.value / 1000000,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
return { ...response, data: { ...response.data, results } };
|
||||
}
|
||||
|
||||
export const convertDataValueToMs = (
|
||||
data?: ConvertibleData,
|
||||
): ConvertibleData | undefined => {
|
||||
data?: SuccessResponse<MetricRangePayloadProps>,
|
||||
): SuccessResponse<MetricRangePayloadProps> | undefined => {
|
||||
const convertedData = data;
|
||||
|
||||
const convertedResult: QueryData[] = data?.payload?.data?.result
|
||||
@@ -53,11 +22,5 @@ export const convertDataValueToMs = (
|
||||
convertedData.payload.data.result = convertedResult;
|
||||
}
|
||||
|
||||
if (convertedData?.rawV5Response) {
|
||||
convertedData.rawV5Response = convertRawV5ValuesToMs(
|
||||
convertedData.rawV5Response,
|
||||
);
|
||||
}
|
||||
|
||||
return convertedData;
|
||||
};
|
||||
|
||||
@@ -22,11 +22,7 @@ import { SuccessResponseV2, Warning } from 'types/api';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
ExecStats,
|
||||
MetricRangePayloadV5,
|
||||
QueryRangeResponseV5,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { ExecStats, MetricRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -196,8 +192,6 @@ export async function GetMetricQueryRange(
|
||||
| SuccessResponseV2<MetricRangePayloadV5>;
|
||||
let warning: Warning | undefined;
|
||||
let meta: ExecStats | undefined;
|
||||
// Raw V5 response, kept before it's converted to legacy — powers client-side export.
|
||||
let rawV5Response: QueryRangeResponseV5 | undefined;
|
||||
|
||||
const panelType = props.originalGraphType || props.graphType;
|
||||
|
||||
@@ -274,8 +268,6 @@ export async function GetMetricQueryRange(
|
||||
endTime: props.end * 1000,
|
||||
});
|
||||
|
||||
rawV5Response = publicResponse.data.data;
|
||||
|
||||
// Convert V5 response to legacy format for components
|
||||
response = convertV5ResponseToLegacy(
|
||||
{
|
||||
@@ -296,8 +288,6 @@ export async function GetMetricQueryRange(
|
||||
headers,
|
||||
);
|
||||
|
||||
rawV5Response = v5Response.data.data;
|
||||
|
||||
// Convert V5 response to legacy format for components
|
||||
response = convertV5ResponseToLegacy(
|
||||
{
|
||||
@@ -376,8 +366,6 @@ export async function GetMetricQueryRange(
|
||||
...response,
|
||||
warning,
|
||||
meta,
|
||||
rawV5Response,
|
||||
legendMap,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -81,18 +81,6 @@ describe('exportTimeseriesData', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('omits display-only format ids (short/none) from headers', () => {
|
||||
const data = [
|
||||
makeQuery('A', [{ series: [makeSeries({ service: 'a' }, [[1000, 1]])] }]),
|
||||
];
|
||||
|
||||
const short = exportTimeseriesData({ data, yAxisUnit: 'short' });
|
||||
expect(short.headers[short.headers.length - 1]).toBe('value');
|
||||
|
||||
const none = exportTimeseriesData({ data, yAxisUnit: 'none' });
|
||||
expect(none.headers[none.headers.length - 1]).toBe('value');
|
||||
});
|
||||
|
||||
it('multi-query: query is its own column; label keys are unioned', () => {
|
||||
const data = [
|
||||
makeQuery('A', [{ series: [makeSeries({ service: 'x' }, [[1000, 1]])] }]),
|
||||
|
||||
@@ -98,16 +98,9 @@ function flatten(
|
||||
return flat;
|
||||
}
|
||||
|
||||
// Display-format ids, not physical units — meaningful on a chart axis
|
||||
// (compact-number formatting) but misleading in an export header.
|
||||
const DISPLAY_ONLY_UNITS = new Set(['short', 'none']);
|
||||
|
||||
// Appends the y-axis unit to the value header: `value` → `value (ms)`.
|
||||
function withUnit(header: string, yAxisUnit?: string): string {
|
||||
if (!yAxisUnit || DISPLAY_ONLY_UNITS.has(yAxisUnit)) {
|
||||
return header;
|
||||
}
|
||||
return `${header} (${yAxisUnit})`;
|
||||
return yAxisUnit ? `${header} (${yAxisUnit})` : header;
|
||||
}
|
||||
|
||||
function toIso(timestamp: number): string {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSelector } from 'react-redux';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
@@ -115,6 +116,9 @@ function TimeSeriesViewContainer({
|
||||
|
||||
return (
|
||||
<div className="trace-explorer-time-series-view-container">
|
||||
<div className="trace-explorer-time-series-view-container-header">
|
||||
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
|
||||
</div>
|
||||
<TimeSeriesView
|
||||
isFilterApplied={isFilterApplied}
|
||||
isError={isError}
|
||||
@@ -122,10 +126,8 @@ function TimeSeriesViewContainer({
|
||||
isLoading={isLoading || isFetching}
|
||||
data={responseData}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onYAxisUnitChange={onUnitChange}
|
||||
dataSource={dataSource}
|
||||
setWarning={setWarning}
|
||||
allowExport
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,7 @@ import {
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
} from '../queryBuilder/queryBuilderData';
|
||||
import {
|
||||
ExecStats,
|
||||
QueryRangeRequestV5,
|
||||
QueryRangeResponseV5,
|
||||
} from '../v5/queryRange';
|
||||
import { ExecStats, QueryRangeRequestV5 } from '../v5/queryRange';
|
||||
import { QueryData, QueryDataV3 } from '../widgets/getQuery';
|
||||
|
||||
export type QueryRangePayload = {
|
||||
@@ -52,9 +48,6 @@ export interface MetricQueryRangeSuccessResponse extends SuccessResponse<
|
||||
> {
|
||||
warning?: Warning;
|
||||
meta?: ExecStats;
|
||||
// Raw V5 response (pre-legacy-conversion) + per-query legend map, for client-side export.
|
||||
rawV5Response?: QueryRangeResponseV5;
|
||||
legendMap?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface MetricRangePayloadV3 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1,.cls-4{fill:#669df6;}.cls-2{fill:#4285f4;}.cls-2,.cls-3,.cls-4{fill-rule:evenodd;}.cls-3{fill:#aecbfa;}</style></defs><title>Icon_24px_MemoryStore_Color</title><g data-name="Product Icons"><g ><rect class="cls-1" x="2" y="3.94" width="3.33" height="2.58"/><rect class="cls-1" x="2" y="8.45" width="3.33" height="2.58"/><rect class="cls-1" x="2" y="12.97" width="3.33" height="2.58"/><rect class="cls-1" x="2" y="17.48" width="3.33" height="2.58"/><rect class="cls-1" x="18.67" y="3.94" width="3.33" height="2.58"/><rect class="cls-1" x="18.67" y="8.45" width="3.33" height="2.58"/><rect class="cls-1" x="18.67" y="12.97" width="3.33" height="2.58"/><rect class="cls-1" x="18.67" y="17.48" width="3.33" height="2.58"/><polygon class="cls-2" points="21.33 6.52 18.67 6.52 18.67 3.94 21.33 6.52"/><polygon class="cls-2" points="21.33 11.03 18.67 11.03 18.67 8.45 21.33 11.03"/><polygon class="cls-2" points="21.33 15.55 18.67 15.55 18.67 12.97 21.33 15.55"/><polygon class="cls-2" points="21.33 20.07 18.67 20.07 18.67 17.48 21.33 20.07"/><path class="cls-3" d="M5.33,22H18.67V2H5.33Zm6-9H8l4.67-7.74V11H16l-4.67,7.74Z"/><polygon class="cls-4" points="11.33 22 11.33 18.77 16 11.03 12.67 11.03 12.67 2 18.67 2 18.67 22 11.33 22"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"id": "memorystore_redis",
|
||||
"title": "GCP Memorystore Redis",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "redis.googleapis.com/server/uptime",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/stats/memory/usage_ratio",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/stats/memory/system_memory_usage_ratio",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/stats/cpu_utilization_main_thread",
|
||||
"unit": "Seconds",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/clients/connected",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/stats/cache_hit_ratio",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/keyspace/keys",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/commands/usec_per_call",
|
||||
"unit": "Microseconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/commands/calls",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/clients/blocked",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "redis.googleapis.com/stats/reject_connections_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "GCP Memorystore Redis Overview",
|
||||
"description": "Overview of GCP Memorystore Redis metrics",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor GCP Memorystore Redis with SigNoz
|
||||
|
||||
Collect key GCP Memorystore Redis metrics and view them with an out of the box dashboard.
|
||||
@@ -21,7 +21,6 @@ func buildClusterRecords(
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts,
|
||||
podPhaseCountsMap map[string]podPhaseCounts,
|
||||
podStatusCounts map[string]podStatusCounts,
|
||||
resourceCounts map[string]map[string]int64,
|
||||
) []inframonitoringtypes.ClusterRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
@@ -75,15 +74,6 @@ func buildClusterRecords(
|
||||
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
|
||||
}
|
||||
|
||||
if counts, ok := resourceCounts[compositeKey]; ok {
|
||||
record.Counts.Nodes = counts[inframonitoringtypes.NodeNameAttrKey]
|
||||
record.Counts.Namespaces = counts[inframonitoringtypes.NamespaceNameAttrKey]
|
||||
record.Counts.Deployments = counts[inframonitoringtypes.DeploymentNameAttrKey]
|
||||
record.Counts.DaemonSets = counts[inframonitoringtypes.DaemonSetNameAttrKey]
|
||||
record.Counts.Jobs = counts[inframonitoringtypes.JobNameAttrKey]
|
||||
record.Counts.StatefulSets = counts[inframonitoringtypes.StatefulSetNameAttrKey]
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
@@ -37,26 +35,6 @@ var clusterAttrKeysForMetadata = []string{
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// clusterCountAttrKeys are the resource attributes whose distinct values are
|
||||
// counted per cluster. Node name is read from the node metric universe, while
|
||||
// namespace + workload names come from the pod metric universe — both unioned
|
||||
// into clusterCountMetricNamesList.
|
||||
var clusterCountAttrKeys = []string{
|
||||
inframonitoringtypes.NodeNameAttrKey,
|
||||
inframonitoringtypes.NamespaceNameAttrKey,
|
||||
inframonitoringtypes.DeploymentNameAttrKey,
|
||||
inframonitoringtypes.DaemonSetNameAttrKey,
|
||||
inframonitoringtypes.JobNameAttrKey,
|
||||
inframonitoringtypes.StatefulSetNameAttrKey,
|
||||
}
|
||||
|
||||
// clusterMetricNamesListForCounts is the metric universe for per-cluster distinct
|
||||
// counts. It unions the pod universe (carries namespace + workload owner labels)
|
||||
// with the cluster/node universe (carries k8s.node.name), so a single query can
|
||||
// count nodes, namespaces, and workloads per cluster. Overlapping pod
|
||||
// phase/status metrics are left in — harmless in a metric_name IN (...) list.
|
||||
var clusterMetricNamesListForCounts = slices.Concat(podsTableMetricNamesList, clustersTableMetricNamesList)
|
||||
|
||||
var orderByToClustersQueryNames = map[string][]string{
|
||||
inframonitoringtypes.ClustersOrderByCPU: {"A"},
|
||||
inframonitoringtypes.ClustersOrderByCPUAllocatable: {"B"},
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
@@ -763,197 +762,3 @@ func (m *module) getMetadata(
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// countAttrIdentityTuples maps a counted attr to the full label tuple that
|
||||
// uniquely identifies its entity, so uniqExact counts real entities rather than
|
||||
// bare names (namespace-scoped workloads with the same name across namespaces,
|
||||
// or cluster-scoped nodes/namespaces across clusters, would otherwise collapse).
|
||||
// Attrs absent from this map are counted on their bare name.
|
||||
var countAttrIdentityTuples = map[string][]string{
|
||||
inframonitoringtypes.NamespaceNameAttrKey: {inframonitoringtypes.ClusterNameAttrKey, inframonitoringtypes.NamespaceNameAttrKey},
|
||||
inframonitoringtypes.NodeNameAttrKey: {inframonitoringtypes.ClusterNameAttrKey, inframonitoringtypes.NodeNameAttrKey},
|
||||
inframonitoringtypes.DeploymentNameAttrKey: {inframonitoringtypes.ClusterNameAttrKey, inframonitoringtypes.NamespaceNameAttrKey, inframonitoringtypes.DeploymentNameAttrKey},
|
||||
inframonitoringtypes.DaemonSetNameAttrKey: {inframonitoringtypes.ClusterNameAttrKey, inframonitoringtypes.NamespaceNameAttrKey, inframonitoringtypes.DaemonSetNameAttrKey},
|
||||
inframonitoringtypes.JobNameAttrKey: {inframonitoringtypes.ClusterNameAttrKey, inframonitoringtypes.NamespaceNameAttrKey, inframonitoringtypes.JobNameAttrKey},
|
||||
inframonitoringtypes.StatefulSetNameAttrKey: {inframonitoringtypes.ClusterNameAttrKey, inframonitoringtypes.NamespaceNameAttrKey, inframonitoringtypes.StatefulSetNameAttrKey},
|
||||
}
|
||||
|
||||
// getPerGroupDistinctCounts returns, per groupBy combination, the exact distinct
|
||||
// count of each attr in attrNames within the time range and metric universe.
|
||||
// It mirrors getMetadata: fingerprints come from the samples table, labels are
|
||||
// read from the timeseries table (raw only, or raw+reduced union when reduction
|
||||
// is enabled), and the user filter is merged with the page-groups IN clauses.
|
||||
// The returned map keys group column values by "\x00", mapping to attr -> count.
|
||||
func (m *module) getPerGroupDistinctCounts(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
start, end int64,
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
attrNames []string,
|
||||
metricNames []string,
|
||||
) (map[string]map[string]int64, error) {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
return map[string]map[string]int64{}, nil
|
||||
}
|
||||
if len(attrNames) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "attrNames must not be empty")
|
||||
}
|
||||
if len(metricNames) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricNames must not be empty")
|
||||
}
|
||||
|
||||
// Merge user filter with page-groups IN clauses.
|
||||
userFilterExpr := ""
|
||||
if filter != nil {
|
||||
userFilterExpr = filter.Expression
|
||||
}
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
reductionEnabled := m.fl.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, featuretypes.NewFlaggerEvaluationContext(orgID))
|
||||
|
||||
// Step-floor the window and pick the right tables — same bounds the QB v5
|
||||
// metric querier uses (see alignedMetricWindow / getMetadata).
|
||||
samplesStartMs, flooredEndMs, tsAdjustedStartMs, distributedTimeSeriesTbl, _, _, localSamplesTbl := alignedMetricWindow(start, end)
|
||||
|
||||
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, localSamplesTbl, samplesStartMs, flooredEndMs)
|
||||
|
||||
groupByCols := make([]string, len(groupBy))
|
||||
for i, key := range groupBy {
|
||||
groupByCols[i] = key.Name
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
// SELECT: one JSONExtractString per groupBy col + one uniqExactIf per attr.
|
||||
selectCols := make([]string, 0, len(groupByCols)+len(attrNames))
|
||||
for _, col := range groupByCols {
|
||||
selectCols = append(selectCols,
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", sb.Var(col), quoteIdentifier(col)),
|
||||
)
|
||||
}
|
||||
for _, attr := range attrNames {
|
||||
// Guard on != '' so a series missing the attr isn't counted as one empty value.
|
||||
extract := fmt.Sprintf("JSONExtractString(labels, %s)", sb.Var(attr))
|
||||
|
||||
// Count on the entity's full identity tuple where one is defined, so
|
||||
// same-named entities in different scopes (e.g. workloads sharing a name
|
||||
// across namespaces) aren't collapsed. Falls back to the bare name.
|
||||
valueExpr := extract
|
||||
if tuple, ok := countAttrIdentityTuples[attr]; ok {
|
||||
parts := make([]string, len(tuple))
|
||||
for i, col := range tuple {
|
||||
parts[i] = fmt.Sprintf("JSONExtractString(labels, %s)", sb.Var(col))
|
||||
}
|
||||
valueExpr = fmt.Sprintf("(%s)", strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
selectCols = append(selectCols,
|
||||
fmt.Sprintf("uniqExactIf(%s, %s != '') AS %s", valueExpr, extract, quoteIdentifier(attr)),
|
||||
)
|
||||
}
|
||||
sb.Select(selectCols...)
|
||||
|
||||
if reductionEnabled {
|
||||
var filterClause *sqlbuilder.WhereClause
|
||||
if mergedFilterExpr != "" {
|
||||
var err error
|
||||
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reducedFpSB := m.buildReducedSamplesTblFingerprintSubQuery(metricNames, samplesStartMs, flooredEndMs)
|
||||
|
||||
rawSrc := sqlbuilder.NewSelectBuilder()
|
||||
rawSrc.Select("labels")
|
||||
rawSrc.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTimeSeriesTbl))
|
||||
rawSrc.Where(
|
||||
rawSrc.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
rawSrc.GE("unix_milli", tsAdjustedStartMs),
|
||||
rawSrc.LE("unix_milli", flooredEndMs),
|
||||
fmt.Sprintf("fingerprint IN (%s)", rawSrc.Var(fpSB)),
|
||||
)
|
||||
if filterClause != nil {
|
||||
rawSrc.AddWhereClause(sqlbuilder.CopyWhereClause(filterClause))
|
||||
}
|
||||
|
||||
reducedSrc := sqlbuilder.NewSelectBuilder()
|
||||
reducedSrc.Select("labels")
|
||||
reducedSrc.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.TimeseriesV4ReducedTableName))
|
||||
reducedSrc.Where(
|
||||
reducedSrc.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
reducedSrc.GE("unix_milli", tsAdjustedStartMs),
|
||||
reducedSrc.LE("unix_milli", flooredEndMs),
|
||||
fmt.Sprintf("fingerprint IN (%s)", reducedSrc.Var(reducedFpSB)),
|
||||
)
|
||||
if filterClause != nil {
|
||||
reducedSrc.AddWhereClause(sqlbuilder.CopyWhereClause(filterClause))
|
||||
}
|
||||
|
||||
sb.From(sb.BuilderAs(sqlbuilder.UnionAll(rawSrc, reducedSrc), "series"))
|
||||
} else {
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTimeSeriesTbl))
|
||||
sb.Where(
|
||||
sb.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
sb.GE("unix_milli", tsAdjustedStartMs),
|
||||
sb.LE("unix_milli", flooredEndMs),
|
||||
fmt.Sprintf("fingerprint IN (%s)", sb.Var(fpSB)),
|
||||
)
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filterClause != nil {
|
||||
sb.AddWhereClause(filterClause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupByAliases := make([]string, 0, len(groupByCols))
|
||||
for _, col := range groupByCols {
|
||||
groupByAliases = append(groupByAliases, quoteIdentifier(col))
|
||||
}
|
||||
sb.GroupBy(groupByAliases...)
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[string]map[string]int64)
|
||||
for rows.Next() {
|
||||
groupVals := make([]string, len(groupByCols))
|
||||
counts := make([]uint64, len(attrNames))
|
||||
|
||||
scanPtrs := make([]any, 0, len(groupByCols)+len(attrNames))
|
||||
for i := range groupVals {
|
||||
scanPtrs = append(scanPtrs, &groupVals[i])
|
||||
}
|
||||
for i := range counts {
|
||||
scanPtrs = append(scanPtrs, &counts[i])
|
||||
}
|
||||
|
||||
if err := rows.Scan(scanPtrs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attrCounts := make(map[string]int64, len(attrNames))
|
||||
for i, attr := range attrNames {
|
||||
attrCounts[attr] = int64(counts[i])
|
||||
}
|
||||
result[compositeKeyFromList(groupVals)] = attrCounts
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -632,7 +632,6 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
phaseCounts map[string]podPhaseCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
@@ -652,17 +651,12 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesTableMetricNamesList)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildNamespaceRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts, resourceCounts)
|
||||
resp.Records = buildNamespaceRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
@@ -738,7 +732,6 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
podPhaseCountsMap map[string]podPhaseCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
@@ -763,17 +756,12 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildClusterRecords(queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCountsMap, podPhaseCountsMap, podStatusCounts, resourceCounts)
|
||||
resp.Records = buildClusterRecords(queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCountsMap, podPhaseCountsMap, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
|
||||
@@ -19,7 +19,6 @@ func buildNamespaceRecords(
|
||||
metadataMap map[string]map[string]string,
|
||||
phaseCounts map[string]podPhaseCounts,
|
||||
podStatusCounts map[string]podStatusCounts,
|
||||
resourceCounts map[string]map[string]int64,
|
||||
) []inframonitoringtypes.NamespaceRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
@@ -58,13 +57,6 @@ func buildNamespaceRecords(
|
||||
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
|
||||
}
|
||||
|
||||
if counts, ok := resourceCounts[compositeKey]; ok {
|
||||
record.Counts.Deployments = counts[inframonitoringtypes.DeploymentNameAttrKey]
|
||||
record.Counts.DaemonSets = counts[inframonitoringtypes.DaemonSetNameAttrKey]
|
||||
record.Counts.Jobs = counts[inframonitoringtypes.JobNameAttrKey]
|
||||
record.Counts.StatefulSets = counts[inframonitoringtypes.StatefulSetNameAttrKey]
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
|
||||
@@ -32,16 +32,6 @@ var namespaceAttrKeysForMetadata = []string{
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// namespaceCountAttrKeys are the workload resource attributes whose distinct
|
||||
// values are counted per namespace. They are read from the pod metric universe,
|
||||
// which carries the owner workload names for each pod series.
|
||||
var namespaceCountAttrKeys = []string{
|
||||
inframonitoringtypes.DeploymentNameAttrKey,
|
||||
inframonitoringtypes.DaemonSetNameAttrKey,
|
||||
inframonitoringtypes.JobNameAttrKey,
|
||||
inframonitoringtypes.StatefulSetNameAttrKey,
|
||||
}
|
||||
|
||||
var orderByToNamespacesQueryNames = map[string][]string{
|
||||
inframonitoringtypes.NamespacesOrderByCPU: {"A"},
|
||||
inframonitoringtypes.NamespacesOrderByMemory: {"D"},
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -247,37 +246,28 @@ func (handler *handler) ReplaceVariables(rw http.ResponseWriter, req *http.Reque
|
||||
switch spec := item.Spec.(type) {
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||
if spec.Filter != nil && spec.Filter.Expression != "" {
|
||||
replaced, warnings, err := variables.ReplaceVariablesInExpression(spec.Filter.Expression, queryRangeRequest.Variables)
|
||||
replaced, err := variables.ReplaceVariablesInExpression(spec.Filter.Expression, queryRangeRequest.Variables)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(warnings) > 0 {
|
||||
handler.set.Logger.WarnContext(req.Context(), "variable replace warnings", slog.Any("warnings", warnings))
|
||||
}
|
||||
spec.Filter.Expression = replaced
|
||||
}
|
||||
queryRangeRequest.CompositeQuery.Queries[idx].Spec = spec
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
if spec.Filter != nil && spec.Filter.Expression != "" {
|
||||
replaced, warnings, err := variables.ReplaceVariablesInExpression(spec.Filter.Expression, queryRangeRequest.Variables)
|
||||
replaced, err := variables.ReplaceVariablesInExpression(spec.Filter.Expression, queryRangeRequest.Variables)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(warnings) > 0 {
|
||||
handler.set.Logger.WarnContext(req.Context(), "variable replace warnings", slog.Any("warnings", warnings))
|
||||
}
|
||||
spec.Filter.Expression = replaced
|
||||
}
|
||||
queryRangeRequest.CompositeQuery.Queries[idx].Spec = spec
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||
if spec.Filter != nil && spec.Filter.Expression != "" {
|
||||
replaced, warnings, err := variables.ReplaceVariablesInExpression(spec.Filter.Expression, queryRangeRequest.Variables)
|
||||
replaced, err := variables.ReplaceVariablesInExpression(spec.Filter.Expression, queryRangeRequest.Variables)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(warnings) > 0 {
|
||||
handler.set.Logger.WarnContext(req.Context(), "variable replace warnings", slog.Any("warnings", warnings))
|
||||
}
|
||||
spec.Filter.Expression = replaced
|
||||
}
|
||||
queryRangeRequest.CompositeQuery.Queries[idx].Spec = spec
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/variables"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
|
||||
sqlbuilder "github.com/huandu/go-sqlbuilder"
|
||||
@@ -429,11 +428,6 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
values = []any{ret}
|
||||
}
|
||||
|
||||
// a value with an embedded variable resolved to __all__ makes the whole condition moot
|
||||
if slices.ContainsFunc(values, isSkipConditionValue) {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
|
||||
if len(values) == 1 {
|
||||
if var_, ok := values[0].(string); ok {
|
||||
// check if this is a variables
|
||||
@@ -449,7 +443,7 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
// we have a variable, now check for dynamic variable
|
||||
if varItem.Type == qbtypes.DynamicVariableType {
|
||||
// check if it is special value to skip entire filter, if so skip it
|
||||
if all_, ok := varItem.Value.(string); ok && all_ == qbtypes.AllVariableValue {
|
||||
if all_, ok := varItem.Value.(string); ok && all_ == "__all__" {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
}
|
||||
@@ -504,11 +498,6 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
value1 := v.Visit(values[0])
|
||||
value2 := v.Visit(values[1])
|
||||
|
||||
// a bound with an embedded variable resolved to __all__ makes the whole condition moot
|
||||
if isSkipConditionValue(value1) || isSkipConditionValue(value2) {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
|
||||
switch value1.(type) {
|
||||
case float64:
|
||||
if _, ok := value2.(float64); !ok {
|
||||
@@ -546,11 +535,6 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
if len(values) > 0 {
|
||||
value := v.Visit(values[0])
|
||||
|
||||
// the value had an embedded variable resolved to __all__; drop the condition
|
||||
if isSkipConditionValue(value) {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
|
||||
if var_, ok := value.(string); ok {
|
||||
// check if this is a variables
|
||||
var ok bool
|
||||
@@ -562,13 +546,6 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
}
|
||||
|
||||
if ok {
|
||||
if varItem.Type == qbtypes.DynamicVariableType {
|
||||
// __all__ used with a single-value operator: there is nothing to
|
||||
// filter on, drop the condition like the IN clause handling does
|
||||
if all_, ok := varItem.Value.(string); ok && all_ == qbtypes.AllVariableValue {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
}
|
||||
switch varValues := varItem.Value.(type) {
|
||||
case []any:
|
||||
if len(varValues) == 0 {
|
||||
@@ -754,11 +731,6 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon
|
||||
|
||||
value := params[1:]
|
||||
|
||||
// a param with an embedded variable resolved to __all__ makes the whole condition moot
|
||||
if slices.ContainsFunc(value, isSkipConditionValue) {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
|
||||
conds, ok := v.buildConditions(key, matchingFieldKeys(key, v.fieldKeys), operator, value)
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
@@ -808,16 +780,7 @@ func (v *filterExpressionVisitor) VisitValue(ctx *grammar.ValueContext) any {
|
||||
if ctx.QUOTED_TEXT() != nil {
|
||||
txt := ctx.QUOTED_TEXT().GetText()
|
||||
// trim quotes and return the value
|
||||
value := trimQuotes(txt)
|
||||
// the string may have variables embedded in it (e.g. '$env-suffix')
|
||||
if strings.Contains(value, "$") {
|
||||
interpolated, hasAll := v.interpolateVariablesInString(value)
|
||||
if hasAll {
|
||||
return skipConditionValue{}
|
||||
}
|
||||
return interpolated
|
||||
}
|
||||
return value
|
||||
return trimQuotes(txt)
|
||||
} else if ctx.NUMBER() != nil {
|
||||
number, err := strconv.ParseFloat(ctx.NUMBER().GetText(), 64)
|
||||
if err != nil {
|
||||
@@ -834,16 +797,7 @@ func (v *filterExpressionVisitor) VisitValue(ctx *grammar.ValueContext) any {
|
||||
// When the user writes an expression like `service.name=redis`
|
||||
// The `redis` part is a VALUE context but parsed as a KEY token
|
||||
// so we return the text as is
|
||||
keyText := ctx.KEY().GetText()
|
||||
// an unquoted value may compose variables too (e.g. $environment-xyz)
|
||||
if strings.Contains(keyText, "$") {
|
||||
interpolated, hasAll := v.interpolateVariablesInString(keyText)
|
||||
if hasAll {
|
||||
return skipConditionValue{}
|
||||
}
|
||||
return interpolated
|
||||
}
|
||||
return keyText
|
||||
return ctx.KEY().GetText()
|
||||
}
|
||||
|
||||
return ErrorConditionLiteral // Should not happen with valid input
|
||||
@@ -944,26 +898,3 @@ func matchingFieldKeys(field *telemetrytypes.TelemetryFieldKey, fieldKeys map[st
|
||||
|
||||
return fieldKeysForName
|
||||
}
|
||||
|
||||
// skipConditionValue is the value-level counterpart of SkipConditionLiteral: VisitValue
|
||||
// returns it when a variable embedded in the value resolved to __all__, and the
|
||||
// comparison handling turns it into SkipConditionLiteral for the whole condition. A
|
||||
// dedicated type (rather than a sentinel string) so no user-supplied value can ever
|
||||
// collide with it.
|
||||
type skipConditionValue struct{}
|
||||
|
||||
// isSkipConditionValue reports whether a visited value says "drop the enclosing
|
||||
// condition" (an embedded variable resolved to __all__).
|
||||
func isSkipConditionValue(value any) bool {
|
||||
_, ok := value.(skipConditionValue)
|
||||
return ok
|
||||
}
|
||||
|
||||
// interpolateVariablesInString delegates to variables.Interpolate (embedded $variable
|
||||
// references, boundary-aware, __all__ detection), folding its warnings into the
|
||||
// visitor. See that function for the full contract.
|
||||
func (v *filterExpressionVisitor) interpolateVariablesInString(s string) (string, bool) {
|
||||
interpolated, hasAll, warnings := variables.Interpolate(s, v.variables)
|
||||
v.addWarnings(warnings, false)
|
||||
return interpolated, hasAll
|
||||
}
|
||||
|
||||
@@ -1552,34 +1552,11 @@ func TestVisitComparison_AllVariable(t *testing.T) {
|
||||
wantSB: "WHERE body_cond",
|
||||
},
|
||||
{
|
||||
// __all__ with a single-value operator means "no filter", same as IN:
|
||||
// the user picked ALL, so the condition is dropped instead of matching
|
||||
// the literal string "__all__".
|
||||
name: "equality with __all__ variable skips condition",
|
||||
// Equality does not trigger __all__ short-circuit; ConditionFor called normally.
|
||||
name: "equality with __all__ variable no shortcircuit",
|
||||
expr: "a = $service",
|
||||
wantRSB: "",
|
||||
wantSB: "",
|
||||
},
|
||||
{
|
||||
// __all__ referenced from inside a composed value drops the condition too.
|
||||
name: "embedded __all__ variable skips condition",
|
||||
expr: "a = '$service-suffix'",
|
||||
wantRSB: "",
|
||||
wantSB: "",
|
||||
},
|
||||
{
|
||||
// Embedded __all__ inside an IN list member drops the condition.
|
||||
name: "embedded __all__ variable in IN list skips condition",
|
||||
expr: "a IN ('$service-suffix', 'other')",
|
||||
wantRSB: "",
|
||||
wantSB: "",
|
||||
},
|
||||
{
|
||||
// Embedded __all__ in one bound of BETWEEN drops the condition.
|
||||
name: "embedded __all__ variable in BETWEEN skips condition",
|
||||
expr: "a BETWEEN '$service-lo' AND 'hi'",
|
||||
wantRSB: "",
|
||||
wantSB: "",
|
||||
wantSB: "WHERE a_cond",
|
||||
},
|
||||
{
|
||||
// __all__→TrueConditionLiteral stripped in AND; y_cond wrapped in paren; NOT wraps.
|
||||
@@ -1852,281 +1829,3 @@ func TestVisitComparison_SkippableLiteralValues(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInterpolateVariablesInString tests the embedded variable interpolation
|
||||
// feature (GitHub issue #10008).
|
||||
func TestInterpolateVariablesInString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
variables map[string]qbtypes.VariableItem
|
||||
expected string
|
||||
expectAll bool
|
||||
wantWarnings int
|
||||
}{
|
||||
{
|
||||
name: "pure variable reference - not interpolated",
|
||||
input: "$service",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"service": {Value: "auth-service"},
|
||||
},
|
||||
expected: "$service", // handled by the standalone variable flow
|
||||
},
|
||||
{
|
||||
name: "variable composed with suffix",
|
||||
input: "$environment-xyz",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"environment": {Value: "prod"},
|
||||
},
|
||||
expected: "prod-xyz",
|
||||
},
|
||||
{
|
||||
name: "variable with prefix and suffix",
|
||||
input: "prefix-$var-suffix",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"var": {Value: "middle"},
|
||||
},
|
||||
expected: "prefix-middle-suffix",
|
||||
},
|
||||
{
|
||||
name: "multiple variables in one string",
|
||||
input: "$region-$env-cluster",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"region": {Value: "us-west"},
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
expected: "us-west-prod-cluster",
|
||||
},
|
||||
{
|
||||
name: "adjacent variables",
|
||||
input: "$env$region",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod-"},
|
||||
"region": {Value: "us"},
|
||||
},
|
||||
expected: "prod-us",
|
||||
},
|
||||
{
|
||||
name: "similar variable names - longer matches first",
|
||||
input: "$env-$environment",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "dev"},
|
||||
"environment": {Value: "production"},
|
||||
},
|
||||
expected: "dev-production",
|
||||
},
|
||||
{
|
||||
name: "unknown variable - preserved as-is",
|
||||
input: "$unknown-suffix",
|
||||
variables: map[string]qbtypes.VariableItem{},
|
||||
expected: "$unknown-suffix",
|
||||
},
|
||||
{
|
||||
name: "shorter variable does not match inside longer unknown reference",
|
||||
input: "$environment",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
expected: "$environment", // NOT "prodironment"
|
||||
},
|
||||
{
|
||||
name: "underscore extends the variable name",
|
||||
input: "$env_name",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
expected: "$env_name",
|
||||
},
|
||||
{
|
||||
name: "digit extends the variable name",
|
||||
input: "$env2",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
expected: "$env2",
|
||||
},
|
||||
{
|
||||
name: "dot is a boundary",
|
||||
input: "$env.internal",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
expected: "prod.internal",
|
||||
},
|
||||
{
|
||||
name: "LIKE pattern wildcards are boundaries",
|
||||
input: "%$env%",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
expected: "%prod%",
|
||||
},
|
||||
{
|
||||
name: "variable with underscore in name",
|
||||
input: "$my_var-test",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"my_var": {Value: "hello"},
|
||||
},
|
||||
expected: "hello-test",
|
||||
},
|
||||
{
|
||||
name: "map key with dollar prefix",
|
||||
input: "$env-xyz",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"$env": {Value: "prod"},
|
||||
},
|
||||
expected: "prod-xyz",
|
||||
},
|
||||
{
|
||||
name: "dollar with no variable after it",
|
||||
input: "cost is 100$",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
expected: "cost is 100$",
|
||||
},
|
||||
{
|
||||
name: "__all__ value reports hasAll",
|
||||
input: "$env-suffix",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "__all__",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
expectAll: true,
|
||||
},
|
||||
{
|
||||
name: "multi-select takes first value with warning",
|
||||
input: "$env-suffix",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: []any{"prod", "staging", "dev"}},
|
||||
},
|
||||
expected: "prod-suffix",
|
||||
wantWarnings: 1,
|
||||
},
|
||||
{
|
||||
name: "numeric variable value",
|
||||
input: "code-$status",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"status": {Value: float64(200)},
|
||||
},
|
||||
expected: "code-200",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
visitor := &filterExpressionVisitor{
|
||||
variables: tt.variables,
|
||||
keysWithWarnings: make(map[string]bool),
|
||||
}
|
||||
result, hasAll := visitor.interpolateVariablesInString(tt.input)
|
||||
assert.Equal(t, tt.expectAll, hasAll)
|
||||
if !tt.expectAll {
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
assert.Len(t, visitor.warnings, tt.wantWarnings)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// valueRecordingConditionBuilder records the value each ConditionFor call receives,
|
||||
// so tests can assert what the visitor actually hands to the condition builder.
|
||||
type valueRecordingConditionBuilder struct {
|
||||
values []any
|
||||
}
|
||||
|
||||
func (b *valueRecordingConditionBuilder) ConditionFor(
|
||||
_ context.Context,
|
||||
_ uint64,
|
||||
_ uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
_ []*telemetrytypes.TelemetryFieldKey,
|
||||
_ qbtypes.FilterOperator,
|
||||
value any,
|
||||
_ *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
b.values = append(b.values, value)
|
||||
return []string{fmt.Sprintf("%s_cond", key.Name)}, nil, nil
|
||||
}
|
||||
|
||||
// TestInterpolatedValueReachesConditionBuilder pins the visitor → condition builder
|
||||
// handoff for embedded variables: the interpolated string (not the raw reference) is
|
||||
// the value passed to ConditionFor. The condition builder is signal-specific but this
|
||||
// seam is not, so it is covered here once instead of per telemetry package.
|
||||
func TestInterpolatedValueReachesConditionBuilder(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expr string
|
||||
variables map[string]qbtypes.VariableItem
|
||||
wantValues []any
|
||||
}{
|
||||
{
|
||||
name: "composed value in quoted string",
|
||||
expr: "a = '$env-xyz'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
wantValues: []any{"prod-xyz"},
|
||||
},
|
||||
{
|
||||
name: "composed value in LIKE pattern",
|
||||
expr: "a LIKE '$env%'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
wantValues: []any{"prod%"},
|
||||
},
|
||||
{
|
||||
name: "pure reference goes through standalone substitution",
|
||||
expr: "a = $env",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
wantValues: []any{"prod"},
|
||||
},
|
||||
{
|
||||
name: "multi-select collapses to first value",
|
||||
expr: "a = '$env-xyz'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: []any{"prod", "staging"}},
|
||||
},
|
||||
wantValues: []any{"prod-xyz"},
|
||||
},
|
||||
{
|
||||
name: "composed members of an IN list",
|
||||
expr: "a IN ('$env-1', '$env-2')",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
wantValues: []any{[]any{"prod-1", "prod-2"}},
|
||||
},
|
||||
{
|
||||
name: "unknown variable passes through untouched",
|
||||
expr: "a = '$unknown-xyz'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Value: "prod"},
|
||||
},
|
||||
wantValues: []any{"$unknown-xyz"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
recorder := &valueRecordingConditionBuilder{}
|
||||
opts := FilterExprVisitorOpts{
|
||||
Context: t.Context(),
|
||||
FieldKeys: visitTestKeys,
|
||||
ConditionBuilder: recorder,
|
||||
Variables: tt.variables,
|
||||
}
|
||||
|
||||
result, err := PrepareWhereClause(tt.expr, opts)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result.IsEmpty())
|
||||
assert.Equal(t, tt.wantValues, recorder.values)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ var (
|
||||
|
||||
// GCP services.
|
||||
GCPServiceCloudSQLPostgres = ServiceID{valuer.NewString("cloudsql_postgres")}
|
||||
GCPServiceMemorystoreRedis = ServiceID{valuer.NewString("memorystore_redis")}
|
||||
)
|
||||
|
||||
func (ServiceID) Enum() []any {
|
||||
@@ -74,6 +75,7 @@ func (ServiceID) Enum() []any {
|
||||
AzureServiceCassandraDB,
|
||||
AzureServiceRedis,
|
||||
GCPServiceCloudSQLPostgres,
|
||||
GCPServiceMemorystoreRedis,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +114,7 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
|
||||
},
|
||||
CloudProviderTypeGCP: {
|
||||
GCPServiceCloudSQLPostgres,
|
||||
GCPServiceMemorystoreRedis,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -28,15 +28,7 @@ type ClusterRecord struct {
|
||||
NodeCountsByReadiness NodeCountsByReadiness `json:"nodeCountsByReadiness" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
Counts struct {
|
||||
Nodes int64 `json:"nodes" required:"true"`
|
||||
Namespaces int64 `json:"namespaces" required:"true"`
|
||||
Deployments int64 `json:"deployments" required:"true"`
|
||||
DaemonSets int64 `json:"daemonSets" required:"true"`
|
||||
Jobs int64 `json:"jobs" required:"true"`
|
||||
StatefulSets int64 `json:"statefulSets" required:"true"`
|
||||
} `json:"counts" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableClusters is the request body for the v2 clusters list API.
|
||||
|
||||
@@ -22,13 +22,7 @@ type NamespaceRecord struct {
|
||||
NamespaceMemory float64 `json:"namespaceMemory" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
Counts struct {
|
||||
Deployments int64 `json:"deployments" required:"true"`
|
||||
DaemonSets int64 `json:"daemonSets" required:"true"`
|
||||
Jobs int64 `json:"jobs" required:"true"`
|
||||
StatefulSets int64 `json:"statefulSets" required:"true"`
|
||||
} `json:"counts" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableNamespaces is the request body for the v2 namespaces list API.
|
||||
|
||||
@@ -314,10 +314,6 @@ func (VariableType) Enum() []any {
|
||||
}
|
||||
}
|
||||
|
||||
// AllVariableValue is the sentinel value a dynamic variable carries when "ALL"
|
||||
// is selected; conditions referencing such a variable are dropped entirely.
|
||||
const AllVariableValue = "__all__"
|
||||
|
||||
type VariableItem struct {
|
||||
Type VariableType `json:"type"`
|
||||
Value any `json:"value"`
|
||||
|
||||
@@ -76,9 +76,9 @@ type PostableRule struct {
|
||||
}
|
||||
|
||||
type NotificationSettings struct {
|
||||
GroupBy []string `json:"groupBy,omitempty"`
|
||||
Renotify *Renotify `json:"renotify,omitempty"`
|
||||
UsePolicy bool `json:"usePolicy,omitempty"`
|
||||
GroupBy []string `json:"groupBy,omitempty"`
|
||||
Renotify Renotify `json:"renotify,omitzero"`
|
||||
UsePolicy bool `json:"usePolicy,omitempty"`
|
||||
// NewGroupEvalDelay is the grace period for new series to be excluded from alerts evaluation
|
||||
NewGroupEvalDelay valuer.TextDuration `json:"newGroupEvalDelay,omitzero"`
|
||||
}
|
||||
@@ -92,7 +92,7 @@ type Renotify struct {
|
||||
func (ns *NotificationSettings) GetAlertManagerNotificationConfig() alertmanagertypes.NotificationConfig {
|
||||
var renotifyInterval time.Duration
|
||||
var noDataRenotifyInterval time.Duration
|
||||
if ns.Renotify != nil && ns.Renotify.Enabled {
|
||||
if ns.Renotify.Enabled {
|
||||
if slices.Contains(ns.Renotify.AlertStates, StateNoData) {
|
||||
noDataRenotifyInterval = ns.Renotify.ReNotifyInterval.Duration()
|
||||
}
|
||||
@@ -204,12 +204,10 @@ func (ns *NotificationSettings) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
// Validate states after unmarshaling
|
||||
if ns.Renotify != nil {
|
||||
for _, state := range ns.Renotify.AlertStates {
|
||||
if state != StateFiring && state != StateNoData {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid alert state: %s", state)
|
||||
for _, state := range ns.Renotify.AlertStates {
|
||||
if state != StateFiring && state != StateNoData {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid alert state: %s", state)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -222,11 +220,6 @@ func (r *PostableRule) processRuleDefaults() {
|
||||
r.SchemaVersion = DefaultSchemaVersion
|
||||
}
|
||||
|
||||
// TODO(srikanthccv): remove as this is now a legacy field
|
||||
if r.Version == "" {
|
||||
r.Version = "v5"
|
||||
}
|
||||
|
||||
// v2alpha1 uses the Evaluation envelope for window/frequency;
|
||||
// only default top-level fields for v1.
|
||||
if r.SchemaVersion != SchemaVersionV2Alpha1 {
|
||||
@@ -278,7 +271,7 @@ func (r *PostableRule) processRuleDefaults() {
|
||||
r.RuleCondition.Thresholds = &thresholdData
|
||||
r.Evaluation = &EvaluationEnvelope{RollingEvaluation, RollingWindow{EvalWindow: r.EvalWindow, Frequency: r.Frequency}}
|
||||
r.NotificationSettings = &NotificationSettings{
|
||||
Renotify: &Renotify{
|
||||
Renotify: Renotify{
|
||||
Enabled: true,
|
||||
ReNotifyInterval: valuer.MustParseTextDuration("4h"),
|
||||
AlertStates: []AlertState{StateFiring},
|
||||
@@ -564,7 +557,7 @@ func (r *PostableRule) validateV2Alpha1() []error {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"notificationSettings: field is required for schemaVersion %q", SchemaVersionV2Alpha1))
|
||||
} else {
|
||||
if r.NotificationSettings.Renotify != nil && r.NotificationSettings.Renotify.Enabled && !r.NotificationSettings.Renotify.ReNotifyInterval.IsPositive() {
|
||||
if r.NotificationSettings.Renotify.Enabled && !r.NotificationSettings.Renotify.ReNotifyInterval.IsPositive() {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"notificationSettings.renotify.interval: must be a positive duration when renotify is enabled"))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
@@ -1328,27 +1327,3 @@ func TestAnomalyNegationEval(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionDefaultsToV5(t *testing.T) {
|
||||
content := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "1", "op": "1"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": {"usePolicy": false}
|
||||
}`
|
||||
|
||||
rule := PostableRule{}
|
||||
require.NoError(t, json.Unmarshal([]byte(content), &rule))
|
||||
assert.Equal(t, "v5", rule.Version)
|
||||
assert.NoError(t, rule.Validate())
|
||||
}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func readBody(t *testing.T, stored string) map[string]json.RawMessage {
|
||||
t.Helper()
|
||||
g := GettableRule{}
|
||||
require.NoError(t, json.Unmarshal([]byte(stored), &g))
|
||||
out, err := json.Marshal(NewRule(&g))
|
||||
require.NoError(t, err)
|
||||
var body map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(out, &body))
|
||||
return body
|
||||
}
|
||||
|
||||
func TestV2RoundTripRolling(t *testing.T) {
|
||||
thresholds := `{
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{"name": "critical", "target": 90.5, "targetUnit": "%", "recoveryTarget": 80.5, "matchType": "at_least_once", "op": "above", "channels": ["slack-critical"]},
|
||||
{"name": "warning", "target": 75, "targetUnit": "%", "matchType": "at_least_once", "op": "above", "channels": ["slack-warnings", "email-oncall"]}
|
||||
]
|
||||
}`
|
||||
thresholdsEchoed := `{
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{"name": "critical", "target": 90.5, "targetUnit": "%", "recoveryTarget": 80.5, "matchType": "at_least_once", "op": "above", "channels": ["slack-critical"]},
|
||||
{"name": "warning", "target": 75, "targetUnit": "%", "recoveryTarget": null, "matchType": "at_least_once", "op": "above", "channels": ["slack-warnings", "email-oncall"]}
|
||||
]
|
||||
}`
|
||||
evaluation := `{"kind": "rolling", "spec": {"evalWindow": "90m", "frequency": "90s"}}`
|
||||
notificationSettings := `{
|
||||
"groupBy": ["service.name", "deployment.environment"],
|
||||
"renotify": {"enabled": true, "interval": "45m", "alertStates": ["firing", "nodata"]},
|
||||
"usePolicy": true,
|
||||
"newGroupEvalDelay": "10m"
|
||||
}`
|
||||
labels := `{"team": "infra", "severity": "critical"}`
|
||||
annotations := `{"summary": "CPU above {{$threshold}}", "description": "value {{$value}}"}`
|
||||
|
||||
stored := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"description": "watches cpu",
|
||||
"ruleType": "promql_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"version": "v5",
|
||||
"disabled": true,
|
||||
"labels": ` + labels + `,
|
||||
"annotations": ` + annotations + `,
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "avg(cpu_usage)"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql",
|
||||
"unit": "percent"
|
||||
},
|
||||
"selectedQueryName": "A",
|
||||
"alertOnAbsent": true,
|
||||
"absentFor": 10,
|
||||
"requireMinPoints": true,
|
||||
"requiredNumPoints": 4,
|
||||
"thresholds": ` + thresholds + `
|
||||
},
|
||||
"evaluation": ` + evaluation + `,
|
||||
"notificationSettings": ` + notificationSettings + `
|
||||
}`
|
||||
|
||||
body := readBody(t, stored)
|
||||
|
||||
assert.JSONEq(t, `"cpu high"`, string(body["alert"]))
|
||||
assert.JSONEq(t, `"METRIC_BASED_ALERT"`, string(body["alertType"]))
|
||||
assert.JSONEq(t, `"watches cpu"`, string(body["description"]))
|
||||
assert.JSONEq(t, `"promql_rule"`, string(body["ruleType"]))
|
||||
assert.JSONEq(t, `"v2alpha1"`, string(body["schemaVersion"]))
|
||||
assert.JSONEq(t, `true`, string(body["disabled"]))
|
||||
assert.JSONEq(t, labels, string(body["labels"]))
|
||||
assert.JSONEq(t, annotations, string(body["annotations"]))
|
||||
assert.JSONEq(t, evaluation, string(body["evaluation"]))
|
||||
assert.JSONEq(t, notificationSettings, string(body["notificationSettings"]))
|
||||
|
||||
var condition map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(body["condition"], &condition))
|
||||
assert.JSONEq(t, thresholdsEchoed, string(condition["thresholds"]))
|
||||
assert.JSONEq(t, `"A"`, string(condition["selectedQueryName"]))
|
||||
assert.JSONEq(t, `true`, string(condition["alertOnAbsent"]))
|
||||
assert.JSONEq(t, `10`, string(condition["absentFor"]))
|
||||
assert.JSONEq(t, `true`, string(condition["requireMinPoints"]))
|
||||
assert.JSONEq(t, `4`, string(condition["requiredNumPoints"]))
|
||||
}
|
||||
|
||||
func TestV2RoundTripCumulative(t *testing.T) {
|
||||
evaluation := `{
|
||||
"kind": "cumulative",
|
||||
"spec": {
|
||||
"schedule": {"type": "daily", "minute": 30, "hour": 9},
|
||||
"frequency": "5m",
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
}`
|
||||
|
||||
stored := `{
|
||||
"alert": "daily budget",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "sum(cost)"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 100, "matchType": "at_least_once", "op": "above"}]}
|
||||
},
|
||||
"evaluation": ` + evaluation + `,
|
||||
"notificationSettings": {"usePolicy": false}
|
||||
}`
|
||||
|
||||
body := readBody(t, stored)
|
||||
assert.JSONEq(t, evaluation, string(body["evaluation"]))
|
||||
}
|
||||
|
||||
func TestV2MinimalReadShape(t *testing.T) {
|
||||
stored := `{
|
||||
"alert": "minimal",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "at_least_once", "op": "above"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": {"usePolicy": false}
|
||||
}`
|
||||
|
||||
body := readBody(t, stored)
|
||||
|
||||
for _, field := range []string{"labels", "annotations", "description", "preferredChannels", "evalWindow", "frequency"} {
|
||||
assert.NotContains(t, body, field)
|
||||
}
|
||||
|
||||
var ns map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(body["notificationSettings"], &ns))
|
||||
for _, field := range []string{"renotify", "groupBy", "newGroupEvalDelay", "usePolicy"} {
|
||||
assert.NotContains(t, ns, field, "notificationSettings.%s", field)
|
||||
}
|
||||
|
||||
assert.JSONEq(t, `false`, string(body["disabled"]))
|
||||
assert.JSONEq(t, `"v5"`, string(body["version"]))
|
||||
|
||||
var condition struct {
|
||||
Thresholds struct {
|
||||
Spec []map[string]json.RawMessage `json:"spec"`
|
||||
} `json:"thresholds"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(body["condition"], &condition))
|
||||
require.Len(t, condition.Thresholds.Spec, 1)
|
||||
spec := condition.Thresholds.Spec[0]
|
||||
assert.JSONEq(t, `""`, string(spec["targetUnit"]))
|
||||
assert.JSONEq(t, `null`, string(spec["channels"]))
|
||||
assert.JSONEq(t, `null`, string(spec["recoveryTarget"]))
|
||||
}
|
||||
|
||||
func TestRenotifyRoundTrip(t *testing.T) {
|
||||
base := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "at_least_once", "op": "above"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": %s
|
||||
}`
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
settings string
|
||||
wantRenotify string
|
||||
}{
|
||||
{
|
||||
name: "absent renotify stays absent",
|
||||
settings: `{"usePolicy": false}`,
|
||||
},
|
||||
{
|
||||
name: "explicitly disabled renotify is echoed",
|
||||
settings: `{"renotify": {"enabled": false}}`,
|
||||
wantRenotify: `{"enabled": false}`,
|
||||
},
|
||||
{
|
||||
name: "enabled renotify with states is echoed",
|
||||
settings: `{"renotify": {"enabled": true, "interval": "30m", "alertStates": ["firing"]}}`,
|
||||
wantRenotify: `{"enabled": true, "interval": "30m", "alertStates": ["firing"]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
body := readBody(t, strings.Replace(base, "%s", tc.settings, 1))
|
||||
var ns map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(body["notificationSettings"], &ns))
|
||||
if tc.wantRenotify == "" {
|
||||
assert.NotContains(t, ns, "renotify")
|
||||
} else {
|
||||
assert.JSONEq(t, tc.wantRenotify, string(ns["renotify"]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchMergePreservesUnpatchedFields(t *testing.T) {
|
||||
stored := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "at_least_once", "op": "above"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": {"renotify": {"enabled": true, "interval": "30m", "alertStates": ["firing"]}}
|
||||
}`
|
||||
|
||||
rule := PostableRule{}
|
||||
require.NoError(t, json.Unmarshal([]byte(stored), &rule))
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"disabled": true}`), &rule))
|
||||
require.NoError(t, rule.Validate())
|
||||
|
||||
assert.True(t, rule.Disabled)
|
||||
assert.NotNil(t, rule.RuleCondition.Thresholds)
|
||||
assert.NotNil(t, rule.Evaluation)
|
||||
require.NotNil(t, rule.NotificationSettings)
|
||||
require.NotNil(t, rule.NotificationSettings.Renotify)
|
||||
assert.True(t, rule.NotificationSettings.Renotify.Enabled)
|
||||
}
|
||||
@@ -169,10 +169,12 @@ func TestValidate_PostableRule_Common(t *testing.T) {
|
||||
errSubstr: "alert",
|
||||
},
|
||||
|
||||
// only "v5" is allowed; missing/empty defaults to "v5"
|
||||
// only "v5" is allowed
|
||||
{
|
||||
name: "missing version defaults to v5",
|
||||
json: removeField(validV1Builder(), "version"),
|
||||
name: "missing version",
|
||||
json: removeField(validV1Builder(), "version"),
|
||||
wantErr: true,
|
||||
errSubstr: "version",
|
||||
},
|
||||
{
|
||||
name: "wrong version v4",
|
||||
@@ -187,8 +189,10 @@ func TestValidate_PostableRule_Common(t *testing.T) {
|
||||
errSubstr: "version",
|
||||
},
|
||||
{
|
||||
name: "empty version defaults to v5",
|
||||
json: patchJSON(validV1Builder(), `{"version": ""}`),
|
||||
name: "empty version",
|
||||
json: patchJSON(validV1Builder(), `{"version": ""}`),
|
||||
wantErr: true,
|
||||
errSubstr: "version",
|
||||
},
|
||||
|
||||
// alert type, capital case to avoid breaking changes
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package variables
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
// isVariableNameChar reports whether c can be part of a variable name.
|
||||
func isVariableNameChar(c byte) bool {
|
||||
return c == '_' || ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
|
||||
}
|
||||
|
||||
// lookupVariable resolves a variable name against the variables map, tolerating a
|
||||
// `$` prefix on either the name or the map keys.
|
||||
func lookupVariable(name string, variables map[string]qbtypes.VariableItem) (qbtypes.VariableItem, bool) {
|
||||
if item, ok := variables[name]; ok {
|
||||
return item, true
|
||||
}
|
||||
if strings.HasPrefix(name, "$") {
|
||||
item, ok := variables[name[1:]]
|
||||
return item, ok
|
||||
}
|
||||
item, ok := variables["$"+name]
|
||||
return item, ok
|
||||
}
|
||||
|
||||
// Interpolate replaces $variable references embedded in s with their values, matching
|
||||
// against the names in the variables map (longest name first). A reference is only
|
||||
// replaced when the character following it cannot extend a variable name: with only
|
||||
// `env` defined, "$env-suffix" becomes "prod-suffix" while "$environment" stays
|
||||
// untouched instead of turning into "prodironment".
|
||||
// A string that is a single variable reference (e.g. "$env") is returned unchanged so
|
||||
// callers can process standalone references with their type intact (lists for IN,
|
||||
// numbers, __all__, etc.).
|
||||
// The returned bool is true when a referenced dynamic variable has the __all__ value,
|
||||
// meaning the enclosing condition must be dropped. Warnings (e.g. a multi-value
|
||||
// variable collapsing to its first value) are returned for the caller to surface.
|
||||
func Interpolate(s string, variables map[string]qbtypes.VariableItem) (string, bool, []string) {
|
||||
if len(variables) == 0 {
|
||||
return s, false, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s, "$") {
|
||||
if _, ok := lookupVariable(s, variables); ok {
|
||||
return s, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(variables))
|
||||
seen := make(map[string]bool, len(variables))
|
||||
for name := range variables {
|
||||
name = strings.TrimPrefix(name, "$")
|
||||
if name == "" || seen[name] {
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
names = append(names, name)
|
||||
}
|
||||
// longest first so that $environment is not mistaken for $env plus a suffix
|
||||
sort.Slice(names, func(i, j int) bool { return len(names[i]) > len(names[j]) })
|
||||
|
||||
var warnings []string
|
||||
var sb strings.Builder
|
||||
i := 0
|
||||
for i < len(s) {
|
||||
if s[i] != '$' {
|
||||
sb.WriteByte(s[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
rest := s[i+1:]
|
||||
matched := false
|
||||
for _, name := range names {
|
||||
if !strings.HasPrefix(rest, name) {
|
||||
continue
|
||||
}
|
||||
// a name character right after the match means this occurrence references
|
||||
// a longer, unknown variable; leave it for a shorter-name check or as-is
|
||||
if len(rest) > len(name) && isVariableNameChar(rest[len(name)]) {
|
||||
continue
|
||||
}
|
||||
varItem, _ := lookupVariable(name, variables)
|
||||
if varItem.Type == qbtypes.DynamicVariableType {
|
||||
if allVal, ok := varItem.Value.(string); ok && allVal == qbtypes.AllVariableValue {
|
||||
return "", true, warnings
|
||||
}
|
||||
}
|
||||
sb.WriteString(formatValueForInterpolation(varItem.Value, name, &warnings))
|
||||
i += 1 + len(name)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
if !matched {
|
||||
sb.WriteByte('$')
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), false, warnings
|
||||
}
|
||||
|
||||
// formatValueForInterpolation renders a variable value as a plain string for embedding
|
||||
// inside a larger value. Multi-value variables collapse to their first value with a
|
||||
// warning, since a pattern like "%$var%" can only hold one.
|
||||
func formatValueForInterpolation(value any, varName string, warnings *[]string) string {
|
||||
switch val := value.(type) {
|
||||
case string:
|
||||
return val
|
||||
case []string:
|
||||
if len(val) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(val) > 1 {
|
||||
*warnings = append(*warnings, fmt.Sprintf("variable `%s` has multiple values, using first value `%s` for string interpolation", varName, val[0]))
|
||||
}
|
||||
return val[0]
|
||||
case []any:
|
||||
if len(val) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(val) > 1 {
|
||||
*warnings = append(*warnings, fmt.Sprintf("variable `%s` has multiple values, using first value for string interpolation", varName))
|
||||
}
|
||||
return formatValueForInterpolation(val[0], varName, warnings)
|
||||
case bool:
|
||||
return strconv.FormatBool(val)
|
||||
default:
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package variables
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -36,22 +35,19 @@ func (e *ErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol
|
||||
type variableReplacementVisitor struct {
|
||||
variables map[string]qbtypes.VariableItem
|
||||
errors []string
|
||||
warnings []string
|
||||
}
|
||||
|
||||
// specialSkipMarker is used to indicate that a condition should be removed.
|
||||
const specialSkipMarker = "__SKIP_CONDITION__"
|
||||
|
||||
// ReplaceVariablesInExpression takes a filter expression and returns it with variables
|
||||
// replaced, along with any warnings generated during the replacement.
|
||||
func ReplaceVariablesInExpression(expression string, variables map[string]qbtypes.VariableItem) (string, []string, error) {
|
||||
// ReplaceVariablesInExpression takes a filter expression and returns it with variables replaced.
|
||||
func ReplaceVariablesInExpression(expression string, variables map[string]qbtypes.VariableItem) (string, error) {
|
||||
input := antlr.NewInputStream(expression)
|
||||
lexer := grammar.NewFilterQueryLexer(input)
|
||||
|
||||
visitor := &variableReplacementVisitor{
|
||||
variables: variables,
|
||||
errors: []string{},
|
||||
warnings: []string{},
|
||||
}
|
||||
|
||||
lexerErrorListener := NewErrorListener()
|
||||
@@ -67,21 +63,21 @@ func ReplaceVariablesInExpression(expression string, variables map[string]qbtype
|
||||
tree := parser.Query()
|
||||
|
||||
if len(parserErrorListener.SyntaxErrors) > 0 {
|
||||
return "", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "syntax errors in expression: %v", parserErrorListener.SyntaxErrors)
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "syntax errors in expression: %v", parserErrorListener.SyntaxErrors)
|
||||
}
|
||||
|
||||
result := visitor.Visit(tree).(string)
|
||||
|
||||
if len(visitor.errors) > 0 {
|
||||
return "", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "errors processing expression: %v", visitor.errors)
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "errors processing expression: %v", visitor.errors)
|
||||
}
|
||||
|
||||
// If the entire expression should be skipped, return empty string
|
||||
if result == specialSkipMarker {
|
||||
return "", visitor.warnings, nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return result, visitor.warnings, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Visit dispatches to the specific visit method based on node type.
|
||||
@@ -473,7 +469,7 @@ func (v *variableReplacementVisitor) VisitValue(ctx *grammar.ValueContext) any {
|
||||
if ok {
|
||||
// Handle dynamic variable with __all__ value
|
||||
if varItem.Type == qbtypes.DynamicVariableType {
|
||||
if allVal, ok := varItem.Value.(string); ok && allVal == qbtypes.AllVariableValue {
|
||||
if allVal, ok := varItem.Value.(string); ok && allVal == "__all__" {
|
||||
// Return special marker to indicate this condition should be removed
|
||||
return specialSkipMarker
|
||||
}
|
||||
@@ -484,18 +480,6 @@ func (v *variableReplacementVisitor) VisitValue(ctx *grammar.ValueContext) any {
|
||||
}
|
||||
}
|
||||
|
||||
// Not a standalone variable reference; the value may still have variables
|
||||
// embedded in it (e.g. '$env-suffix' or 'prefix-$var-suffix')
|
||||
if strings.Contains(originalValue, "$") {
|
||||
interpolated, hasAll := v.interpolateVariablesInString(originalValue)
|
||||
if hasAll {
|
||||
return specialSkipMarker
|
||||
}
|
||||
if interpolated != originalValue {
|
||||
return v.formatVariableValue(interpolated)
|
||||
}
|
||||
}
|
||||
|
||||
// Return original value if not a variable or variable not found
|
||||
// If it was quoted text and not a variable, return with quotes
|
||||
if ctx.QUOTED_TEXT() != nil && !strings.HasPrefix(originalValue, "$") {
|
||||
@@ -521,7 +505,7 @@ func (v *variableReplacementVisitor) VisitKey(ctx *grammar.KeyContext) any {
|
||||
if ok {
|
||||
// Handle dynamic variable with __all__ value
|
||||
if varItem.Type == qbtypes.DynamicVariableType {
|
||||
if allVal, ok := varItem.Value.(string); ok && allVal == qbtypes.AllVariableValue {
|
||||
if allVal, ok := varItem.Value.(string); ok && allVal == "__all__" {
|
||||
return specialSkipMarker
|
||||
}
|
||||
}
|
||||
@@ -533,25 +517,6 @@ func (v *variableReplacementVisitor) VisitKey(ctx *grammar.KeyContext) any {
|
||||
return keyText
|
||||
}
|
||||
|
||||
// addWarning appends w unless it is already recorded; comparisons visit their values
|
||||
// twice (once to check for __all__, once to rebuild), which would duplicate warnings.
|
||||
func (v *variableReplacementVisitor) addWarning(w string) {
|
||||
if slices.Contains(v.warnings, w) {
|
||||
return
|
||||
}
|
||||
v.warnings = append(v.warnings, w)
|
||||
}
|
||||
|
||||
// interpolateVariablesInString delegates to Interpolate, folding its warnings into
|
||||
// the visitor.
|
||||
func (v *variableReplacementVisitor) interpolateVariablesInString(s string) (string, bool) {
|
||||
interpolated, hasAll, warnings := Interpolate(s, v.variables)
|
||||
for _, w := range warnings {
|
||||
v.addWarning(w)
|
||||
}
|
||||
return interpolated, hasAll
|
||||
}
|
||||
|
||||
// formatVariableValue formats a variable value for inclusion in the expression.
|
||||
func (v *variableReplacementVisitor) formatVariableValue(value any) string {
|
||||
switch val := value.(type) {
|
||||
|
||||
@@ -421,154 +421,11 @@ func TestReplaceVariablesInExpression(t *testing.T) {
|
||||
},
|
||||
expected: "message NOT CONTAINS 'debug'",
|
||||
},
|
||||
{
|
||||
name: "variable composed with suffix in value",
|
||||
expression: "cluster_name = '$environment-xyz'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"environment": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
expected: "cluster_name = 'prod-xyz'",
|
||||
},
|
||||
{
|
||||
name: "variable composed with suffix without quotes",
|
||||
expression: "cluster_name = $environment-xyz",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"environment": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "staging",
|
||||
},
|
||||
},
|
||||
expected: "cluster_name = 'staging-xyz'",
|
||||
},
|
||||
{
|
||||
name: "variable in LIKE pattern with suffix",
|
||||
expression: "service.name LIKE '$env%'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
expected: "service.name LIKE 'prod%'",
|
||||
},
|
||||
{
|
||||
name: "variable composed with prefix and suffix",
|
||||
expression: "label = 'prefix-$var-suffix'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"var": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "middle",
|
||||
},
|
||||
},
|
||||
expected: "label = 'prefix-middle-suffix'",
|
||||
},
|
||||
{
|
||||
name: "multiple variables in one string",
|
||||
expression: "path = '$region-$env-cluster'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"region": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "us-west",
|
||||
},
|
||||
"env": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
expected: "path = 'us-west-prod-cluster'",
|
||||
},
|
||||
{
|
||||
name: "embedded variable with __all__ value skips condition",
|
||||
expression: "cluster_name = '$environment-xyz'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"environment": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "__all__",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "variable with underscore composed with suffix",
|
||||
expression: "name = '$my_var-test'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"my_var": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "hello",
|
||||
},
|
||||
},
|
||||
expected: "name = 'hello-test'",
|
||||
},
|
||||
{
|
||||
name: "similar variable names - longer matches first",
|
||||
expression: "name = '$env-$environment'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "dev",
|
||||
},
|
||||
"environment": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "production",
|
||||
},
|
||||
},
|
||||
expected: "name = 'dev-production'",
|
||||
},
|
||||
{
|
||||
// Unresolved $-prefixed values come back unquoted so they stay
|
||||
// variable-shaped for downstream resolution.
|
||||
name: "shorter variable does not match inside longer unknown reference",
|
||||
expression: "name = '$environment'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
// `env` must not corrupt `$environment` into 'prodironment'
|
||||
expected: "name = $environment",
|
||||
},
|
||||
{
|
||||
name: "unknown embedded variable left untouched",
|
||||
expression: "name = '$unknown-suffix'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
expected: "name = $unknown-suffix",
|
||||
},
|
||||
{
|
||||
name: "embedded variable in unquoted value with prefix",
|
||||
expression: "cluster_name = abc-$env",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {
|
||||
Type: qbtypes.DynamicVariableType,
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
expected: "cluster_name = 'abc-prod'",
|
||||
},
|
||||
{
|
||||
name: "multi-select variable in composed string takes first value",
|
||||
expression: "cluster_name = '$env-xyz'",
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {
|
||||
Type: qbtypes.CustomVariableType,
|
||||
Value: []any{"prod", "staging"},
|
||||
},
|
||||
},
|
||||
expected: "cluster_name = 'prod-xyz'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, _, err := ReplaceVariablesInExpression(tt.expression, tt.variables)
|
||||
result, err := ReplaceVariablesInExpression(tt.expression, tt.variables)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
@@ -581,24 +438,6 @@ func TestReplaceVariablesInExpression(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestReplaceVariablesInExpressionWarnings ensures multi-value variables used inside a
|
||||
// composed string produce exactly one warning, even though comparisons visit their
|
||||
// values twice (once for the __all__ check, once to rebuild the expression).
|
||||
func TestReplaceVariablesInExpressionWarnings(t *testing.T) {
|
||||
variables := map[string]qbtypes.VariableItem{
|
||||
"env": {
|
||||
Type: qbtypes.CustomVariableType,
|
||||
Value: []any{"prod", "staging"},
|
||||
},
|
||||
}
|
||||
|
||||
result, warnings, err := ReplaceVariablesInExpression("cluster_name = '$env-xyz'", variables)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "cluster_name = 'prod-xyz'", result)
|
||||
assert.Len(t, warnings, 1)
|
||||
assert.Contains(t, warnings[0], "`env` has multiple values")
|
||||
}
|
||||
|
||||
func TestFormatVariableValue(t *testing.T) {
|
||||
visitor := &variableReplacementVisitor{}
|
||||
|
||||
|
||||
25
tests/fixtures/querier.py
vendored
25
tests/fixtures/querier.py
vendored
@@ -972,28 +972,3 @@ def make_scalar_query_request(
|
||||
"formatOptions": {"formatTableResultForUI": True, "fillGaps": False},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def make_substitute_vars_request(
|
||||
signoz: types.SigNoz,
|
||||
token: str,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
queries: list[dict],
|
||||
variables: dict,
|
||||
timeout: int = QUERY_TIMEOUT,
|
||||
) -> requests.Response:
|
||||
return requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v5/substitute_vars"),
|
||||
timeout=timeout,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"schemaVersion": "v1",
|
||||
"start": start_ms,
|
||||
"end": end_ms,
|
||||
"requestType": "time_series",
|
||||
"compositeQuery": {"queries": queries},
|
||||
"variables": variables,
|
||||
"formatOptions": {"formatTableResultForUI": False, "fillGaps": False},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n1","k8s.node.uid":"acc-c1-n1-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n1","k8s.node.uid":"acc-c1-n1-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n1","k8s.node.uid":"acc-c1-n1-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1","k8s.deployment.name":"dep-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1","k8s.deployment.name":"dep-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1","k8s.deployment.name":"dep-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
@@ -31,12 +31,9 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1","k8s.statefulset.name":"sts-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1","k8s.statefulset.name":"sts-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1","k8s.statefulset.name":"sts-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p2-uid","k8s.pod.name":"acc-c1-n2-p2","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1","k8s.deployment.name":"dep-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p2-uid","k8s.pod.name":"acc-c1-n2-p2","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1","k8s.deployment.name":"dep-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p2-uid","k8s.pod.name":"acc-c1-n2-p2","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1","k8s.deployment.name":"dep-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
@@ -52,9 +49,9 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2","k8s.deployment.name":"dep-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2","k8s.deployment.name":"dep-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2","k8s.deployment.name":"dep-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
@@ -70,9 +67,9 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2","k8s.daemonset.name":"ds-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2","k8s.daemonset.name":"ds-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2","k8s.daemonset.name":"ds-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
@@ -88,6 +85,6 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2","k8s.job.name":"job-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2","k8s.job.name":"job-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2","k8s.job.name":"job-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
|
||||
@@ -12,18 +12,10 @@
|
||||
},
|
||||
"podCountsByPhase": {
|
||||
"pending": 0,
|
||||
"running": 3,
|
||||
"running": 2,
|
||||
"succeeded": 0,
|
||||
"failed": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"counts": {
|
||||
"nodes": 2,
|
||||
"namespaces": 2,
|
||||
"deployments": 2,
|
||||
"daemonSets": 0,
|
||||
"jobs": 0,
|
||||
"statefulSets": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -42,14 +34,6 @@
|
||||
"succeeded": 0,
|
||||
"failed": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"counts": {
|
||||
"nodes": 3,
|
||||
"namespaces": 2,
|
||||
"deployments": 1,
|
||||
"daemonSets": 1,
|
||||
"jobs": 1,
|
||||
"statefulSets": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
"namespaceName": "acc-ns-1",
|
||||
"namespaceCPU": 1.0,
|
||||
"namespaceMemory": 200000000.0,
|
||||
"podCountsByPhase": {"pending": 0, "running": 2, "succeeded": 0, "failed": 0, "unknown": 0},
|
||||
"counts": {"deployments": 2, "daemonSets": 0, "jobs": 0, "statefulSets": 0}
|
||||
"podCountsByPhase": {"pending": 0, "running": 2, "succeeded": 0, "failed": 0, "unknown": 0}
|
||||
},
|
||||
{
|
||||
"namespaceName": "acc-ns-2",
|
||||
"namespaceCPU": 2.25,
|
||||
"namespaceMemory": 600000000.0,
|
||||
"podCountsByPhase": {"pending": 0, "running": 3, "succeeded": 0, "failed": 0, "unknown": 0},
|
||||
"counts": {"deployments": 1, "daemonSets": 1, "jobs": 1, "statefulSets": 0}
|
||||
"podCountsByPhase": {"pending": 0, "running": 3, "succeeded": 0, "failed": 0, "unknown": 0}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ def test_namespaces_accuracy(
|
||||
"namespaceCPU",
|
||||
"namespaceMemory",
|
||||
"podCountsByPhase",
|
||||
"counts",
|
||||
"meta",
|
||||
):
|
||||
assert field in record, f"missing {field} in {record!r}"
|
||||
@@ -82,9 +81,6 @@ def test_namespaces_accuracy(
|
||||
for bucket in ("pending", "running", "succeeded", "failed", "unknown"):
|
||||
assert bucket in record["podCountsByPhase"]
|
||||
assert isinstance(record["podCountsByPhase"][bucket], int)
|
||||
for bucket in ("deployments", "daemonSets", "jobs", "statefulSets"):
|
||||
assert bucket in record["counts"]
|
||||
assert isinstance(record["counts"][bucket], int)
|
||||
|
||||
assert record["meta"].get("k8s.namespace.name") == record["namespaceName"]
|
||||
assert "k8s.cluster.name" in record["meta"]
|
||||
@@ -94,7 +90,6 @@ def test_namespaces_accuracy(
|
||||
for field in ("namespaceCPU", "namespaceMemory"):
|
||||
assert compare_values(record[field], exp[field], 1e-6), f"{record['namespaceName']}.{field}: got {record[field]}, expected {exp[field]}"
|
||||
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
|
||||
assert record["counts"] == exp["counts"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -78,7 +78,6 @@ def test_clusters_accuracy(
|
||||
"clusterMemoryAllocatable",
|
||||
"nodeCountsByReadiness",
|
||||
"podCountsByPhase",
|
||||
"counts",
|
||||
"meta",
|
||||
):
|
||||
assert field in record, f"missing {field} in {record!r}"
|
||||
@@ -89,9 +88,6 @@ def test_clusters_accuracy(
|
||||
for bucket in ("pending", "running", "succeeded", "failed", "unknown"):
|
||||
assert bucket in record["podCountsByPhase"]
|
||||
assert isinstance(record["podCountsByPhase"][bucket], int)
|
||||
for bucket in ("nodes", "namespaces", "deployments", "daemonSets", "jobs", "statefulSets"):
|
||||
assert bucket in record["counts"]
|
||||
assert isinstance(record["counts"][bucket], int)
|
||||
|
||||
assert record["meta"].get("k8s.cluster.name") == record["clusterName"]
|
||||
|
||||
@@ -106,7 +102,6 @@ def test_clusters_accuracy(
|
||||
assert compare_values(record[field], exp[field], 1e-6), f"{record['clusterName']}.{field}: got {record[field]}, expected {exp[field]}"
|
||||
assert record["nodeCountsByReadiness"] == exp["nodeCountsByReadiness"]
|
||||
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
|
||||
assert record["counts"] == exp["counts"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
"""
|
||||
Tests for the /api/v5/substitute_vars endpoint (GitHub issue #10008).
|
||||
|
||||
Variables are supported both as standalone RHS values (field = $var, field IN $var)
|
||||
and composed inside values ('$env-suffix', LIKE '$env%').
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import Aggregation, BuilderQuery, make_substitute_vars_request
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expression,variables,expected",
|
||||
[
|
||||
pytest.param(
|
||||
"service.name = $service",
|
||||
{"service": {"type": "query", "value": "auth-service"}},
|
||||
"service.name = 'auth-service'",
|
||||
id="standalone_variable",
|
||||
),
|
||||
pytest.param(
|
||||
"cluster_name = '$environment-xyz'",
|
||||
{"environment": {"type": "custom", "value": "prod"}},
|
||||
"cluster_name = 'prod-xyz'",
|
||||
id="variable_in_string",
|
||||
),
|
||||
pytest.param(
|
||||
"service.name = $service AND env = $environment",
|
||||
{
|
||||
"service": {"type": "text", "value": "auth-service"},
|
||||
"environment": {"type": "query", "value": "production"},
|
||||
},
|
||||
"service.name = 'auth-service' AND env = 'production'",
|
||||
id="multiple_variables",
|
||||
),
|
||||
pytest.param(
|
||||
"service.name IN $services",
|
||||
{"services": {"type": "query", "value": ["auth-service", "api-service"]}},
|
||||
"service.name IN ['auth-service', 'api-service']",
|
||||
id="array_variable",
|
||||
),
|
||||
pytest.param(
|
||||
"service.name LIKE '$env%'",
|
||||
{"env": {"type": "text", "value": "prod"}},
|
||||
"service.name LIKE 'prod%'",
|
||||
id="like_pattern",
|
||||
),
|
||||
pytest.param(
|
||||
"cluster_name = $environment-xyz",
|
||||
{"environment": {"type": "dynamic", "value": "staging"}},
|
||||
"cluster_name = 'staging-xyz'",
|
||||
id="variable_without_quotes",
|
||||
),
|
||||
# `env` must not match inside the longer, undefined `$environment` reference.
|
||||
# Unresolved $-prefixed values come back unquoted so they stay variable-shaped.
|
||||
pytest.param(
|
||||
"cluster_name = '$environment-xyz'",
|
||||
{"env": {"type": "custom", "value": "prod"}},
|
||||
"cluster_name = $environment-xyz",
|
||||
id="unknown_embedded_variable_untouched",
|
||||
),
|
||||
# expression should be empty when __all__ is used
|
||||
pytest.param(
|
||||
"service.name = $service",
|
||||
{"service": {"type": "dynamic", "value": "__all__"}},
|
||||
"",
|
||||
id="all_value_standalone",
|
||||
),
|
||||
pytest.param(
|
||||
"cluster_name = '$environment-xyz'",
|
||||
{"environment": {"type": "dynamic", "value": "__all__"}},
|
||||
"",
|
||||
id="all_value_in_composed_string",
|
||||
),
|
||||
# only the env condition should remain
|
||||
pytest.param(
|
||||
"service.name = $service AND env = $env",
|
||||
{
|
||||
"service": {"type": "dynamic", "value": "__all__"},
|
||||
"env": {"type": "dynamic", "value": "production"},
|
||||
},
|
||||
"env = 'production'",
|
||||
id="all_value_partial",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_substitute_vars(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
expression: str,
|
||||
variables: dict,
|
||||
expected: str,
|
||||
) -> None:
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
now = datetime.now(tz=UTC)
|
||||
|
||||
response = make_substitute_vars_request(
|
||||
signoz,
|
||||
token,
|
||||
int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
int(now.timestamp() * 1000),
|
||||
[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
aggregations=[Aggregation("count()")],
|
||||
step_interval=60,
|
||||
filter_expression=expression,
|
||||
).to_dict()
|
||||
],
|
||||
variables,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
queries = response.json()["data"]["compositeQuery"]["queries"]
|
||||
assert len(queries) == 1
|
||||
assert queries[0]["spec"]["filter"]["expression"] == expected
|
||||
@@ -1,274 +0,0 @@
|
||||
"""
|
||||
End-to-end tests for variables in /api/v5/query_range filter expressions
|
||||
(GitHub issue #10008): standalone ($var, IN $var), composed inside values
|
||||
('$env-suffix', LIKE '$env%'), and the special __all__ value.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.querier import (
|
||||
BuilderQuery,
|
||||
OrderBy,
|
||||
TelemetryFieldKey,
|
||||
get_column_data_from_response,
|
||||
get_scalar_table_data,
|
||||
make_query_request,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expression,variables,expected_logs",
|
||||
[
|
||||
pytest.param(
|
||||
"service.name = $service",
|
||||
{"service": {"type": "query", "value": "auth-service"}},
|
||||
{"auth-prod-log"},
|
||||
id="standalone_variable",
|
||||
),
|
||||
pytest.param(
|
||||
"service.name IN $services",
|
||||
{"services": {"type": "custom", "value": ["auth-service", "api-service"]}},
|
||||
{"auth-prod-log", "api-prod-log"},
|
||||
id="array_variable",
|
||||
),
|
||||
pytest.param(
|
||||
"cluster_name = '$environment-xyz'",
|
||||
{"environment": {"type": "dynamic", "value": "prod"}},
|
||||
{"auth-prod-log", "api-prod-log"},
|
||||
id="variable_composed_in_string",
|
||||
),
|
||||
pytest.param(
|
||||
"cluster_name LIKE '$env%'",
|
||||
{"env": {"type": "text", "value": "prod"}},
|
||||
{"auth-prod-log", "api-prod-log"},
|
||||
id="variable_in_like_pattern",
|
||||
),
|
||||
pytest.param(
|
||||
"path = '$region-$env-cluster'",
|
||||
{
|
||||
"region": {"type": "query", "value": "us-west"},
|
||||
"env": {"type": "custom", "value": "prod"},
|
||||
},
|
||||
{"auth-prod-log"},
|
||||
id="multiple_variables_in_string",
|
||||
),
|
||||
pytest.param(
|
||||
"label = 'prefix-$var-suffix'",
|
||||
{"var": {"type": "text", "value": "middle"}},
|
||||
{"auth-prod-log"},
|
||||
id="variable_with_prefix_and_suffix",
|
||||
),
|
||||
pytest.param(
|
||||
"cluster_name = $environment-xyz",
|
||||
{"environment": {"type": "custom", "value": "staging"}},
|
||||
{"web-staging-log"},
|
||||
id="variable_without_quotes",
|
||||
),
|
||||
pytest.param(
|
||||
"service.name = $service",
|
||||
{"service": {"type": "query", "value": "auth-service"}},
|
||||
{"auth-prod-log"},
|
||||
id="query_variable_type",
|
||||
),
|
||||
pytest.param(
|
||||
"service.name = $service",
|
||||
{"service": {"type": "custom", "value": "auth-service"}},
|
||||
{"auth-prod-log"},
|
||||
id="custom_variable_type",
|
||||
),
|
||||
pytest.param(
|
||||
"service.name = $service",
|
||||
{"service": {"type": "text", "value": "auth-service"}},
|
||||
{"auth-prod-log"},
|
||||
id="text_variable_type",
|
||||
),
|
||||
pytest.param(
|
||||
"service.name = $service",
|
||||
{"service": {"type": "dynamic", "value": "auth-service"}},
|
||||
{"auth-prod-log"},
|
||||
id="dynamic_variable_type",
|
||||
),
|
||||
# `__all__` removes the filter condition entirely → all logs match
|
||||
pytest.param(
|
||||
"service.name = $service",
|
||||
{"service": {"type": "dynamic", "value": "__all__"}},
|
||||
{"auth-prod-log", "api-prod-log", "web-staging-log"},
|
||||
id="all_value_standalone",
|
||||
),
|
||||
pytest.param(
|
||||
"cluster_name = '$environment-xyz'",
|
||||
{"environment": {"type": "dynamic", "value": "__all__"}},
|
||||
{"auth-prod-log", "api-prod-log", "web-staging-log"},
|
||||
id="all_value_in_composed_string",
|
||||
),
|
||||
pytest.param(
|
||||
"service.name IN $services",
|
||||
{"services": {"type": "dynamic", "value": "__all__"}},
|
||||
{"auth-prod-log", "api-prod-log", "web-staging-log"},
|
||||
id="all_value_in_array",
|
||||
),
|
||||
# `__all__` for service drops only that condition; env filter still applies
|
||||
pytest.param(
|
||||
"service.name = $service AND env = $env",
|
||||
{
|
||||
"service": {"type": "dynamic", "value": "__all__"},
|
||||
"env": {"type": "dynamic", "value": "production"},
|
||||
},
|
||||
{"auth-prod-log", "api-prod-log"},
|
||||
id="all_value_partial_filter",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_variable_in_query(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
expression: str,
|
||||
variables: dict,
|
||||
expected_logs: set[str],
|
||||
) -> None:
|
||||
"""
|
||||
Runs a raw logs query with the filter expression and variables, then checks the
|
||||
set of matching log bodies.
|
||||
|
||||
Canonical dataset:
|
||||
auth-prod-log: service.name=auth-service, env=production, cluster_name=prod-xyz,
|
||||
path=us-west-prod-cluster, label=prefix-middle-suffix
|
||||
api-prod-log: service.name=api-service, env=production, cluster_name=prod-xyz,
|
||||
path=us-east-prod-cluster, label=prefix-other-suffix
|
||||
web-staging-log: service.name=web-service, env=staging, cluster_name=staging-xyz,
|
||||
path=eu-west-staging-cluster
|
||||
"""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_logs(
|
||||
[
|
||||
Logs(
|
||||
timestamp=now - timedelta(seconds=5),
|
||||
body="auth-prod-log",
|
||||
resources={"service.name": "auth-service"},
|
||||
attributes={
|
||||
"env": "production",
|
||||
"cluster_name": "prod-xyz",
|
||||
"path": "us-west-prod-cluster",
|
||||
"label": "prefix-middle-suffix",
|
||||
},
|
||||
),
|
||||
Logs(
|
||||
timestamp=now - timedelta(seconds=4),
|
||||
body="api-prod-log",
|
||||
resources={"service.name": "api-service"},
|
||||
attributes={
|
||||
"env": "production",
|
||||
"cluster_name": "prod-xyz",
|
||||
"path": "us-east-prod-cluster",
|
||||
"label": "prefix-other-suffix",
|
||||
},
|
||||
),
|
||||
Logs(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
body="web-staging-log",
|
||||
resources={"service.name": "web-service"},
|
||||
attributes={
|
||||
"env": "staging",
|
||||
"cluster_name": "staging-xyz",
|
||||
"path": "eu-west-staging-cluster",
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
now = datetime.now(tz=UTC)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
request_type="raw",
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
limit=100,
|
||||
filter_expression=expression,
|
||||
order=[
|
||||
OrderBy(key=TelemetryFieldKey(name="timestamp"), direction="desc"),
|
||||
OrderBy(key=TelemetryFieldKey(name="id"), direction="desc"),
|
||||
],
|
||||
).to_dict()
|
||||
],
|
||||
variables=variables,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
assert set(get_column_data_from_response(response.json(), "body")) == expected_logs
|
||||
|
||||
|
||||
def test_variable_in_query_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
"""
|
||||
A composed variable in the filter works together with groupBy: count logs
|
||||
with cluster_name = '$env-xyz' grouped by service.name.
|
||||
"""
|
||||
now = datetime.now(tz=UTC)
|
||||
logs: list[Logs] = []
|
||||
for i in range(3):
|
||||
for service, cluster in [
|
||||
("auth-service", "prod-xyz"),
|
||||
("api-service", "prod-xyz"),
|
||||
("web-service", "staging-xyz"),
|
||||
]:
|
||||
logs.append(
|
||||
Logs(
|
||||
timestamp=now - timedelta(seconds=i + 1),
|
||||
body=f"{service} log {i}",
|
||||
resources={"service.name": service},
|
||||
attributes={"cluster_name": cluster},
|
||||
)
|
||||
)
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
now = datetime.now(tz=UTC)
|
||||
|
||||
# BuilderQuery has no groupBy field, so the spec is built inline
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
request_type="scalar",
|
||||
queries=[
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"disabled": False,
|
||||
"filter": {"expression": "cluster_name = '$env-xyz'"},
|
||||
"groupBy": [{"name": "service.name", "fieldContext": "resource"}],
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
variables={"env": {"type": "query", "value": "prod"}},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
rows = get_scalar_table_data(response.json())
|
||||
assert sorted(rows) == [["api-service", 3], ["auth-service", 3]]
|
||||
Reference in New Issue
Block a user