Compare commits

..

5 Commits

Author SHA1 Message Date
srikanthccv
6218e27ec2 chore(ruletypes): update rule schema with required fields and remove v1 fields 2026-07-14 14:46:16 +05:30
Srikanth Chekuri
16c1fce326 fix(ruletypes): address rule read round-trip gaps (#12110)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-07-14 08:36:30 +00:00
Gaurav Tewari
06b762b7ef fix(llm o11y): update unpriced models tab test case (#12108)
* fix: update unpriced models tab test case

* chore: update ci timming

* chore: update comment

* chore: remove comment

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-14 08:22:03 +00:00
Aditya Singh
466edf1f1c feat: timeseries export in logs, traces and metrics explorers (2) (#12055)
* feat(data-export): surface raw V5 response and legend map from GetMetricQueryRange

Keeps the pre-conversion V5 queryRange response and the per-query legend map on the returned MetricQueryRangeSuccessResponse so explorer views can feed client-side export without a refetch. Additive optional fields; the legacy V3 render path is untouched. One chokepoint covers Logs, Traces and Metrics.

* fix(data-export): omit display-only format ids from export headers

'short'/'none' are Grafana-style display formats, not physical units — meaningful on a chart axis but misleading as 'value (short)' in a CSV header. Real units (ms, bytes, ...) are unchanged.

* feat(data-export): timeseries export menu and opt-in TimeSeriesView header

New TimeseriesExportMenu (csv/jsonl, LONG shape default; same look as DownloadOptionsMenu, which stays raw/list-only) rendered from a new opt-in TimeSeriesView header row: unit selector left (onYAxisUnitChange — rendering only, unit state stays with each view), download right (allowExport, needs the surfaced raw V5). Both default off, so views opt in explicitly. Includes gating + menu tests.

* feat(explorer): enable timeseries export in Logs, Traces and Metrics

Logs and Traces pass allowExport + onYAxisUnitChange and drop their external unit-filter headers (one header row: unit left, download right). Metrics passes allowExport only, per chart — its YAxisUnitSelector stays screen-level (persists to the metric, spans N charts). convertDataValueToMs now converts the raw V5 tree too, so Traces duration exports carry the ms values the chart shows instead of raw nanoseconds.

* refactor(data-export): use @signozhq/ui components in the export popover

Review feedback: swap antd Button/Popover/Tooltip for the design-system equivalents (radix-style Popover with controlled open, TooltipSimple, Button with native loading). Styles restructured for the DS DOM with periscope font tokens; test renders via tests/test-utils for the TooltipProvider.
2026-07-14 06:54:51 +00:00
Nikhil Mantri
849353b5c0 feat(infra-monitoring): counts query for counting related entities (#12000)
* chore: added types and open api spec changes

* chore: added method to calculate reason

* chore: per group pod status counts with req metric checks method added

* chore: wired up pod status counts

* chore: pod restarts type added

* chore: added restart counts for the group

* chore: bug in query fix

* chore: onboarding API changes

* chore: integration tests added

* chore: added podcountsbyphase in other entities

* chore: added pod status counts for other entities

* chore: added integration tests for other entities

* chore: added checks api changes for other entities

* chore: rearrangement

* chore: removed succeeded status and mark it as completed

* chore: query beautified

* chore: corrected metrics list for metadata lookup

* chore: removed dead constants

* chore: goroutines for ListHosts

* chore: goroutines for ListPods

* chore: goroutines for ListNodes

* chore: goroutines for ListNamespaces

* chore: goroutines for ListClusters

* chore: goroutines for ListDeployments

* chore: goroutines for ListStatefulsets

* chore: added goroutines for ListStatefulsets, ListJobs and ListDaemonsets

* chore: added function

* chore: added struct changes

* chore: added count attr keys

* chore: wired counts to the response fields

* chore: regenerated API spec

* chore: merged main, resolved conflicts

* chore: nodes count surfacing

* chore: integration tests added

* fix: use tuple mapping to uniquely identify attrs:

* chore: integration tests update
2026-07-14 06:38:27 +00:00
43 changed files with 1333 additions and 2620 deletions

View File

@@ -1495,7 +1495,6 @@ components:
- cassandradb
- redis
- cloudsql_postgres
- memorystore_redis
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -4260,6 +4259,34 @@ 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
@@ -4280,6 +4307,7 @@ components:
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesClusters:
@@ -4801,6 +4829,26 @@ 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
@@ -4824,6 +4872,7 @@ components:
- namespaceMemory
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesNamespaces:
@@ -7366,6 +7415,7 @@ components:
queries:
items:
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
minItems: 1
nullable: true
type: array
queryType:
@@ -7552,35 +7602,28 @@ components:
type: string
disabled:
type: boolean
evalWindow:
type: string
evaluation:
$ref: '#/components/schemas/RuletypesEvaluationEnvelope'
frequency:
type: string
labels:
additionalProperties:
type: string
type: object
notificationSettings:
$ref: '#/components/schemas/RuletypesNotificationSettings'
preferredChannels:
items:
type: string
type: array
ruleType:
$ref: '#/components/schemas/RuletypesRuleType'
schemaVersion:
type: string
source:
type: string
version:
enum:
- v2alpha1
type: string
required:
- alert
- alertType
- ruleType
- condition
- schemaVersion
- evaluation
- notificationSettings
type: object
RuletypesQueryType:
enum:
@@ -7630,12 +7673,8 @@ components:
type: string
disabled:
type: boolean
evalWindow:
type: string
evaluation:
$ref: '#/components/schemas/RuletypesEvaluationEnvelope'
frequency:
type: string
id:
type: string
labels:
@@ -7644,15 +7683,11 @@ components:
type: object
notificationSettings:
$ref: '#/components/schemas/RuletypesNotificationSettings'
preferredChannels:
items:
type: string
type: array
ruleType:
$ref: '#/components/schemas/RuletypesRuleType'
schemaVersion:
type: string
source:
enum:
- v2alpha1
type: string
state:
$ref: '#/components/schemas/RuletypesAlertState'
@@ -7661,8 +7696,6 @@ components:
type: string
updatedBy:
type: string
version:
type: string
required:
- id
- state
@@ -7670,6 +7703,9 @@ components:
- alertType
- ruleType
- condition
- schemaVersion
- evaluation
- notificationSettings
type: object
RuletypesRuleCondition:
properties:
@@ -7682,10 +7718,6 @@ components:
type: string
compositeQuery:
$ref: '#/components/schemas/RuletypesAlertCompositeQuery'
matchType:
$ref: '#/components/schemas/RuletypesMatchType'
op:
$ref: '#/components/schemas/RuletypesCompareOperator'
requireMinPoints:
type: boolean
requiredNumPoints:
@@ -7694,16 +7726,12 @@ components:
$ref: '#/components/schemas/RuletypesSeasonality'
selectedQueryName:
type: string
target:
format: double
nullable: true
type: number
targetUnit:
type: string
thresholds:
$ref: '#/components/schemas/RuletypesRuleThresholdData'
required:
- compositeQuery
- thresholds
- selectedQueryName
type: object
RuletypesRuleThresholdData:
discriminator:

View File

@@ -2814,7 +2814,6 @@ export enum CloudintegrationtypesServiceIDDTO {
cassandradb = 'cassandradb',
redis = 'redis',
cloudsql_postgres = 'cloudsql_postgres',
memorystore_redis = 'memorystore_redis',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -5670,6 +5669,39 @@ 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;
};
@@ -5814,6 +5846,10 @@ export interface InframonitoringtypesClusterRecordDTO {
* @type string
*/
clusterName: string;
/**
* @type object
*/
counts: InframonitoringtypesClusterRecordDTOCounts;
/**
* @type object,null
*/
@@ -6369,6 +6405,29 @@ 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;
};
@@ -6380,6 +6439,10 @@ export type InframonitoringtypesNamespaceRecordDTOMeta =
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
export interface InframonitoringtypesNamespaceRecordDTO {
/**
* @type object
*/
counts: InframonitoringtypesNamespaceRecordDTOCounts;
/**
* @type object,null
*/

View File

@@ -89,21 +89,20 @@ 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(trigger).getByText('openai:gpt-4o ($3.00/$9.00)'),
within(screen.getByTestId(`map-to-select-${MODEL}`)).getByText(
'openai:gpt-4o ($3.00/$9.00)',
),
).toBeInTheDocument();
await user.click(await screen.findByTestId('unpriced-map-cancel-btn'));
await waitFor(() =>
await waitFor(() => {
const trigger = screen.getByTestId(`map-to-select-${MODEL}`);
expect(
within(trigger).queryByText('openai:gpt-4o ($3.00/$9.00)'),
).not.toBeInTheDocument(),
);
expect(
within(trigger).getByText('Select / Create a pricing model'),
).toBeInTheDocument();
within(trigger).getByText('Select / Create a pricing model'),
).toBeInTheDocument();
});
});
it('commits the mapping in one request when confirmed', async () => {

View File

@@ -191,14 +191,6 @@
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;

View File

@@ -32,7 +32,6 @@ 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';
@@ -461,18 +460,17 @@ 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>
)}

View File

@@ -311,6 +311,7 @@ function TimeSeries({
dataSource={DataSource.METRICS}
error={queries[index].error as APIError}
setWarning={setWarning}
allowExport
/>
</div>
);

View File

@@ -3,6 +3,14 @@
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;

View File

@@ -16,6 +16,7 @@ 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';
@@ -41,11 +42,14 @@ 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({
@@ -59,6 +63,8 @@ function TimeSeriesView({
setWarning,
panelType = PANEL_TYPES.TIME_SERIES,
stackBarChart = false,
allowExport = false,
onYAxisUnitChange,
}: TimeSeriesViewProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
@@ -244,10 +250,33 @@ 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%' }}
@@ -295,7 +324,11 @@ function TimeSeriesView({
}
interface TimeSeriesViewProps {
data?: SuccessResponse<MetricRangePayloadProps> & { warning?: Warning };
data?: SuccessResponse<MetricRangePayloadProps> & {
warning?: Warning;
rawV5Response?: QueryRangeResponseV5;
legendMap?: Record<string, string>;
};
yAxisUnit?: string;
isLoading: boolean;
isError: boolean;
@@ -305,6 +338,11 @@ 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 = {

View File

@@ -0,0 +1,33 @@
.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%;
}
}

View File

@@ -0,0 +1,91 @@
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>
);
}

View File

@@ -0,0 +1,138 @@
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();
});
});

View File

@@ -0,0 +1,84 @@
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();
});
});

View File

@@ -1,10 +1,41 @@
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?: SuccessResponse<MetricRangePayloadProps>,
): SuccessResponse<MetricRangePayloadProps> | undefined => {
data?: ConvertibleData,
): ConvertibleData | undefined => {
const convertedData = data;
const convertedResult: QueryData[] = data?.payload?.data?.result
@@ -22,5 +53,11 @@ export const convertDataValueToMs = (
convertedData.payload.data.result = convertedResult;
}
if (convertedData?.rawV5Response) {
convertedData.rawV5Response = convertRawV5ValuesToMs(
convertedData.rawV5Response,
);
}
return convertedData;
};

View File

@@ -22,7 +22,11 @@ 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 } from 'types/api/v5/queryRange';
import {
ExecStats,
MetricRangePayloadV5,
QueryRangeResponseV5,
} from 'types/api/v5/queryRange';
import { QueryData } from 'types/api/widgets/getQuery';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
@@ -192,6 +196,8 @@ 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;
@@ -268,6 +274,8 @@ export async function GetMetricQueryRange(
endTime: props.end * 1000,
});
rawV5Response = publicResponse.data.data;
// Convert V5 response to legacy format for components
response = convertV5ResponseToLegacy(
{
@@ -288,6 +296,8 @@ export async function GetMetricQueryRange(
headers,
);
rawV5Response = v5Response.data.data;
// Convert V5 response to legacy format for components
response = convertV5ResponseToLegacy(
{
@@ -366,6 +376,8 @@ export async function GetMetricQueryRange(
...response,
warning,
meta,
rawV5Response,
legendMap,
};
}

View File

@@ -81,6 +81,18 @@ 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]])] }]),

View File

@@ -98,9 +98,16 @@ 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 {
return yAxisUnit ? `${header} (${yAxisUnit})` : header;
if (!yAxisUnit || DISPLAY_ONLY_UNITS.has(yAxisUnit)) {
return header;
}
return `${header} (${yAxisUnit})`;
}
function toIso(timestamp: number): string {

View File

@@ -10,7 +10,6 @@ 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';
@@ -116,9 +115,6 @@ 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}
@@ -126,8 +122,10 @@ function TimeSeriesViewContainer({
isLoading={isLoading || isFetching}
data={responseData}
yAxisUnit={yAxisUnit}
onYAxisUnitChange={onUnitChange}
dataSource={dataSource}
setWarning={setWarning}
allowExport
/>
</div>
);

View File

@@ -8,7 +8,11 @@ import {
IClickHouseQuery,
IPromQLQuery,
} from '../queryBuilder/queryBuilderData';
import { ExecStats, QueryRangeRequestV5 } from '../v5/queryRange';
import {
ExecStats,
QueryRangeRequestV5,
QueryRangeResponseV5,
} from '../v5/queryRange';
import { QueryData, QueryDataV3 } from '../widgets/getQuery';
export type QueryRangePayload = {
@@ -48,6 +52,9 @@ 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 {

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,94 +0,0 @@
{
"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"
}
]
}
}

View File

@@ -1,3 +0,0 @@
### Monitor GCP Memorystore Redis with SigNoz
Collect key GCP Memorystore Redis metrics and view them with an out of the box dashboard.

View File

@@ -21,6 +21,7 @@ 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)
@@ -74,6 +75,15 @@ 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

View File

@@ -1,6 +1,8 @@
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"
@@ -35,6 +37,26 @@ 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"},

View File

@@ -11,6 +11,7 @@ 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"
@@ -762,3 +763,197 @@ 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
}

View File

@@ -632,6 +632,7 @@ 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)
@@ -651,12 +652,17 @@ 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)
resp.Records = buildNamespaceRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts, resourceCounts)
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
return resp, nil
@@ -732,6 +738,7 @@ 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)
@@ -756,12 +763,17 @@ 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)
resp.Records = buildClusterRecords(queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCountsMap, podPhaseCountsMap, podStatusCounts, resourceCounts)
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
return resp, nil

View File

@@ -19,6 +19,7 @@ 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)
@@ -57,6 +58,13 @@ 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

View File

@@ -32,6 +32,16 @@ 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"},

View File

@@ -42,7 +42,6 @@ var (
// GCP services.
GCPServiceCloudSQLPostgres = ServiceID{valuer.NewString("cloudsql_postgres")}
GCPServiceMemorystoreRedis = ServiceID{valuer.NewString("memorystore_redis")}
)
func (ServiceID) Enum() []any {
@@ -75,7 +74,6 @@ func (ServiceID) Enum() []any {
AzureServiceCassandraDB,
AzureServiceRedis,
GCPServiceCloudSQLPostgres,
GCPServiceMemorystoreRedis,
}
}
@@ -114,7 +112,6 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
},
CloudProviderTypeGCP: {
GCPServiceCloudSQLPostgres,
GCPServiceMemorystoreRedis,
},
}

View File

@@ -28,7 +28,15 @@ type ClusterRecord struct {
NodeCountsByReadiness NodeCountsByReadiness `json:"nodeCountsByReadiness" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
Meta map[string]string `json:"meta" 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"`
}
// PostableClusters is the request body for the v2 clusters list API.

View File

@@ -22,7 +22,13 @@ type NamespaceRecord struct {
NamespaceMemory float64 `json:"namespaceMemory" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
Meta map[string]string `json:"meta" 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"`
}
// PostableNamespaces is the request body for the v2 namespaces list API.

View File

@@ -100,7 +100,7 @@ func (QueryType) Enum() []any {
}
type AlertCompositeQuery struct {
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true"`
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true" minItems:"1"`
PanelType PanelType `json:"panelType" required:"true"`
QueryType QueryType `json:"queryType" required:"true"`

View File

@@ -76,9 +76,9 @@ type PostableRule struct {
}
type NotificationSettings struct {
GroupBy []string `json:"groupBy,omitempty"`
Renotify Renotify `json:"renotify,omitzero"`
UsePolicy bool `json:"usePolicy,omitempty"`
GroupBy []string `json:"groupBy,omitempty"`
Renotify *Renotify `json:"renotify,omitempty"`
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.Enabled {
if ns.Renotify != nil && ns.Renotify.Enabled {
if slices.Contains(ns.Renotify.AlertStates, StateNoData) {
noDataRenotifyInterval = ns.Renotify.ReNotifyInterval.Duration()
}
@@ -204,10 +204,12 @@ func (ns *NotificationSettings) UnmarshalJSON(data []byte) error {
}
// Validate states after unmarshaling
for _, state := range ns.Renotify.AlertStates {
if state != StateFiring && state != StateNoData {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid alert state: %s", state)
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)
}
}
}
return nil
@@ -220,6 +222,11 @@ 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 {
@@ -271,7 +278,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},
@@ -557,7 +564,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.Enabled && !r.NotificationSettings.Renotify.ReNotifyInterval.IsPositive() {
if r.NotificationSettings.Renotify != nil && 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"))
}

View File

@@ -6,6 +6,7 @@ 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"
@@ -1327,3 +1328,27 @@ 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())
}

View File

@@ -0,0 +1,46 @@
package ruletypes
import (
"github.com/swaggest/jsonschema-go"
)
var (
_ jsonschema.Preparer = (*PostableRule)(nil)
_ jsonschema.Preparer = (*RuleCondition)(nil)
)
// PrepareJSONSchema restricts the published rule schema to the v2alpha1
// contract: the v1-only fields (evalWindow, frequency, preferredChannels) and
// the query version (always "v5", defaulted by the server) are omitted, and
// the fields the server requires for schemaVersion v2alpha1 are marked
// required. Runtime JSON handling is unchanged and continues to load stored
// v1 rules until schemaVersion v1 is removed.
//
// Rule and GettableRule embed PostableRule, so this also applies to the
// published read models through method promotion.
func (r *PostableRule) PrepareJSONSchema(schema *jsonschema.Schema) error {
for _, name := range []string{"evalWindow", "frequency", "preferredChannels", "version", "source"} {
delete(schema.Properties, name)
}
schema.Required = append(schema.Required, "schemaVersion", "evaluation", "notificationSettings")
if prop, ok := schema.Properties["schemaVersion"]; ok && prop.TypeObject != nil {
prop.TypeObject.WithEnum(SchemaVersionV2Alpha1)
}
return nil
}
// PrepareJSONSchema removes the v1-only condition fields (target, op,
// matchType, targetUnit — expressed per threshold entry in v2alpha1) from the
// published schema and marks thresholds and selectedQueryName required.
func (rc *RuleCondition) PrepareJSONSchema(schema *jsonschema.Schema) error {
for _, name := range []string{"target", "op", "matchType", "targetUnit"} {
delete(schema.Properties, name)
}
schema.Required = append(schema.Required, "thresholds", "selectedQueryName")
return nil
}

View File

@@ -0,0 +1,258 @@
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)
}

View File

@@ -169,12 +169,10 @@ func TestValidate_PostableRule_Common(t *testing.T) {
errSubstr: "alert",
},
// only "v5" is allowed
// only "v5" is allowed; missing/empty defaults to "v5"
{
name: "missing version",
json: removeField(validV1Builder(), "version"),
wantErr: true,
errSubstr: "version",
name: "missing version defaults to v5",
json: removeField(validV1Builder(), "version"),
},
{
name: "wrong version v4",
@@ -189,10 +187,8 @@ func TestValidate_PostableRule_Common(t *testing.T) {
errSubstr: "version",
},
{
name: "empty version",
json: patchJSON(validV1Builder(), `{"version": ""}`),
wantErr: true,
errSubstr: "version",
name: "empty version defaults to v5",
json: patchJSON(validV1Builder(), `{"version": ""}`),
},
// alert type, capital case to avoid breaking changes

View File

@@ -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"},"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.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.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,9 +31,12 @@
{"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-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.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.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}
@@ -49,9 +52,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"},"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.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.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}
@@ -67,9 +70,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"},"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.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.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}
@@ -85,6 +88,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-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}
{"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}

View File

@@ -12,10 +12,18 @@
},
"podCountsByPhase": {
"pending": 0,
"running": 2,
"running": 3,
"succeeded": 0,
"failed": 0,
"unknown": 0
},
"counts": {
"nodes": 2,
"namespaces": 2,
"deployments": 2,
"daemonSets": 0,
"jobs": 0,
"statefulSets": 1
}
},
{
@@ -34,6 +42,14 @@
"succeeded": 0,
"failed": 0,
"unknown": 0
},
"counts": {
"nodes": 3,
"namespaces": 2,
"deployments": 1,
"daemonSets": 1,
"jobs": 1,
"statefulSets": 0
}
}
]

View File

@@ -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"},"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}
{"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}

View File

@@ -4,13 +4,15 @@
"namespaceName": "acc-ns-1",
"namespaceCPU": 1.0,
"namespaceMemory": 200000000.0,
"podCountsByPhase": {"pending": 0, "running": 2, "succeeded": 0, "failed": 0, "unknown": 0}
"podCountsByPhase": {"pending": 0, "running": 2, "succeeded": 0, "failed": 0, "unknown": 0},
"counts": {"deployments": 2, "daemonSets": 0, "jobs": 0, "statefulSets": 0}
},
{
"namespaceName": "acc-ns-2",
"namespaceCPU": 2.25,
"namespaceMemory": 600000000.0,
"podCountsByPhase": {"pending": 0, "running": 3, "succeeded": 0, "failed": 0, "unknown": 0}
"podCountsByPhase": {"pending": 0, "running": 3, "succeeded": 0, "failed": 0, "unknown": 0},
"counts": {"deployments": 1, "daemonSets": 1, "jobs": 1, "statefulSets": 0}
}
]
}

View File

@@ -74,6 +74,7 @@ def test_namespaces_accuracy(
"namespaceCPU",
"namespaceMemory",
"podCountsByPhase",
"counts",
"meta",
):
assert field in record, f"missing {field} in {record!r}"
@@ -81,6 +82,9 @@ 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"]
@@ -90,6 +94,7 @@ 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(

View File

@@ -78,6 +78,7 @@ def test_clusters_accuracy(
"clusterMemoryAllocatable",
"nodeCountsByReadiness",
"podCountsByPhase",
"counts",
"meta",
):
assert field in record, f"missing {field} in {record!r}"
@@ -88,6 +89,9 @@ 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"]
@@ -102,6 +106,7 @@ 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(