mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-11 20:50:35 +01:00
Compare commits
1 Commits
traceop
...
service-fg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19962459cb |
@@ -2520,65 +2520,6 @@ components:
|
||||
enabled:
|
||||
type: boolean
|
||||
type: object
|
||||
InframonitoringtypesClusterRecord:
|
||||
properties:
|
||||
clusterCPU:
|
||||
format: double
|
||||
type: number
|
||||
clusterCPUAllocatable:
|
||||
format: double
|
||||
type: number
|
||||
clusterMemory:
|
||||
format: double
|
||||
type: number
|
||||
clusterMemoryAllocatable:
|
||||
format: double
|
||||
type: number
|
||||
clusterName:
|
||||
type: string
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
nodeCountsByReadiness:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
required:
|
||||
- clusterName
|
||||
- clusterCPU
|
||||
- clusterCPUAllocatable
|
||||
- clusterMemory
|
||||
- clusterMemoryAllocatable
|
||||
- nodeCountsByReadiness
|
||||
- podCountsByPhase
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesClusters:
|
||||
properties:
|
||||
endTimeBeforeRetention:
|
||||
type: boolean
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
$ref: '#/components/schemas/InframonitoringtypesResponseType'
|
||||
warning:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
|
||||
required:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesHostFilter:
|
||||
properties:
|
||||
expression:
|
||||
@@ -2883,32 +2824,6 @@ components:
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesPostableClusters:
|
||||
properties:
|
||||
end:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
nullable: true
|
||||
type: array
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
orderBy:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
|
||||
start:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- start
|
||||
- end
|
||||
- limit
|
||||
type: object
|
||||
InframonitoringtypesPostableHosts:
|
||||
properties:
|
||||
end:
|
||||
@@ -11831,77 +11746,6 @@ paths:
|
||||
summary: Health check
|
||||
tags:
|
||||
- health
|
||||
/api/v2/infra_monitoring/clusters:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'Returns a paginated list of Kubernetes clusters with key aggregated
|
||||
metrics derived by summing per-node values within the group: CPU usage, CPU
|
||||
allocatable, memory working set, memory allocatable. Each row also reports
|
||||
per-group nodeCountsByReadiness ({ ready, notReady } from each node''s latest
|
||||
k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending,
|
||||
running, succeeded, failed, unknown } from each pod''s latest k8s.pod.phase
|
||||
value). Each cluster includes metadata attributes (k8s.cluster.name). The
|
||||
response type is ''list'' for the default k8s.cluster.name grouping or ''grouped_list''
|
||||
for custom groupBy keys; in both modes every row aggregates nodes and pods
|
||||
in the group. Supports filtering via a filter expression, custom groupBy,
|
||||
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
|
||||
via offset/limit. Also reports missing required metrics and whether the requested
|
||||
time range falls before the data retention boundary. Numeric metric fields
|
||||
(clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable)
|
||||
return -1 as a sentinel when no data is available for that field.'
|
||||
operationId: ListClusters
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPostableClusters'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusters'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List Clusters for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/hosts:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -12,12 +12,10 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
InframonitoringtypesPostableClustersDTO,
|
||||
InframonitoringtypesPostableHostsDTO,
|
||||
InframonitoringtypesPostableNamespacesDTO,
|
||||
InframonitoringtypesPostableNodesDTO,
|
||||
InframonitoringtypesPostablePodsDTO,
|
||||
ListClusters200,
|
||||
ListHosts200,
|
||||
ListNamespaces200,
|
||||
ListNodes200,
|
||||
@@ -28,90 +26,6 @@ import type {
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
|
||||
* @summary List Clusters for Infra Monitoring
|
||||
*/
|
||||
export const listClusters = (
|
||||
inframonitoringtypesPostableClustersDTO: BodyType<InframonitoringtypesPostableClustersDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListClusters200>({
|
||||
url: `/api/v2/infra_monitoring/clusters`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: inframonitoringtypesPostableClustersDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListClustersMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listClusters>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableClustersDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listClusters>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableClustersDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['listClusters'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof listClusters>>,
|
||||
{ data: BodyType<InframonitoringtypesPostableClustersDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return listClusters(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ListClustersMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listClusters>>
|
||||
>;
|
||||
export type ListClustersMutationBody =
|
||||
BodyType<InframonitoringtypesPostableClustersDTO>;
|
||||
export type ListClustersMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List Clusters for Infra Monitoring
|
||||
*/
|
||||
export const useListClusters = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listClusters>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableClustersDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof listClusters>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableClustersDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getListClustersMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.
|
||||
* @summary List Hosts for Infra Monitoring
|
||||
|
||||
@@ -4568,66 +4568,6 @@ export interface GlobaltypesTokenizerConfigDTO {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesClusterRecordDTOMeta = {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
|
||||
export interface InframonitoringtypesClusterRecordDTO {
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
clusterCPU: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
clusterCPUAllocatable: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
clusterMemory: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
clusterMemoryAllocatable: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
clusterName: string;
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
meta: InframonitoringtypesClusterRecordDTOMeta;
|
||||
nodeCountsByReadiness: InframonitoringtypesNodeCountsByReadinessDTO;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesClustersDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
records: InframonitoringtypesClusterRecordDTO[] | null;
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total: number;
|
||||
type: InframonitoringtypesResponseTypeDTO;
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesHostFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -4945,34 +4885,6 @@ export interface InframonitoringtypesPodsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableClustersDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
offset?: number;
|
||||
orderBy?: Querybuildertypesv5OrderByDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableHostsDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -9412,14 +9324,6 @@ export type Healthz503 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListClusters200 = {
|
||||
data: InframonitoringtypesClustersDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListHosts200 = {
|
||||
data: InframonitoringtypesHostsDTO;
|
||||
/**
|
||||
|
||||
@@ -2,9 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { LoadingOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -517,9 +516,7 @@ export default function CeleryOverviewTable({
|
||||
bordered={false}
|
||||
loading={{
|
||||
spinning: isLoading,
|
||||
indicator: (
|
||||
<Spin indicator={<Loader size={14} className="animate-spin" />} />
|
||||
),
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CaretDownOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { CaretDownOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { Modal, Select, Spin, Tooltip, Tree, TreeDataNode } from 'antd';
|
||||
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -223,7 +222,7 @@ function AttributeCheckList({
|
||||
>
|
||||
{loading ? (
|
||||
<div className="loader-container">
|
||||
<Spin indicator={<Loader className="animate-spin" />} size="large" />
|
||||
<Spin indicator={<LoadingOutlined spin />} size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="modal-content">
|
||||
|
||||
@@ -7,9 +7,12 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { DownOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
DownOutlined,
|
||||
LoadingOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Button, Checkbox, Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
@@ -1697,7 +1700,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
{loading && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
<Loader className="animate-spin" />
|
||||
<LoadingOutlined />
|
||||
</div>
|
||||
<div className="navigation-text">Refreshing values...</div>
|
||||
</div>
|
||||
@@ -1705,7 +1708,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
{!loading && waitingMessage && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
<Loader className="animate-spin" />
|
||||
<LoadingOutlined />
|
||||
</div>
|
||||
<div className="navigation-text" title={waitingMessage}>
|
||||
{waitingMessage}
|
||||
|
||||
@@ -6,9 +6,13 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { CloseOutlined, DownOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
CloseOutlined,
|
||||
DownOutlined,
|
||||
LoadingOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Select } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
@@ -577,7 +581,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
{loading && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
<Loader className="animate-spin" />
|
||||
<LoadingOutlined />
|
||||
</div>
|
||||
<div className="navigation-text">Refreshing values...</div>
|
||||
</div>
|
||||
@@ -585,7 +589,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
{!loading && waitingMessage && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
<Loader className="animate-spin" />
|
||||
<LoadingOutlined />
|
||||
</div>
|
||||
<div className="navigation-text" title={waitingMessage}>
|
||||
{waitingMessage}
|
||||
|
||||
@@ -16,8 +16,8 @@ interface OverviewTabProps {
|
||||
account: ServiceAccountRow;
|
||||
localName: string;
|
||||
onNameChange: (v: string) => void;
|
||||
localRoles: string[];
|
||||
onRolesChange: (v: string[]) => void;
|
||||
localRole: string;
|
||||
onRoleChange: (v: string | undefined) => void;
|
||||
isDisabled: boolean;
|
||||
availableRoles: AuthtypesRoleDTO[];
|
||||
rolesLoading?: boolean;
|
||||
@@ -31,8 +31,8 @@ function OverviewTab({
|
||||
account,
|
||||
localName,
|
||||
onNameChange,
|
||||
localRoles,
|
||||
onRolesChange,
|
||||
localRole,
|
||||
onRoleChange,
|
||||
isDisabled,
|
||||
availableRoles,
|
||||
rolesLoading,
|
||||
@@ -95,15 +95,10 @@ function OverviewTab({
|
||||
{isDisabled ? (
|
||||
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
|
||||
<div className="sa-drawer__disabled-roles">
|
||||
{localRoles.length > 0 ? (
|
||||
localRoles.map((roleId) => {
|
||||
const role = availableRoles.find((r) => r.id === roleId);
|
||||
return (
|
||||
<Badge key={roleId} color="vanilla">
|
||||
{role?.name ?? roleId}
|
||||
</Badge>
|
||||
);
|
||||
})
|
||||
{localRole ? (
|
||||
<Badge color="vanilla">
|
||||
{availableRoles.find((r) => r.id === localRole)?.name ?? localRole}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="sa-drawer__input-text">—</span>
|
||||
)}
|
||||
@@ -113,15 +108,14 @@ function OverviewTab({
|
||||
) : (
|
||||
<RolesSelect
|
||||
id="sa-roles"
|
||||
mode="multiple"
|
||||
roles={availableRoles}
|
||||
loading={rolesLoading}
|
||||
isError={rolesError}
|
||||
error={rolesErrorObj}
|
||||
onRefetch={onRefetchRoles}
|
||||
value={localRoles}
|
||||
onChange={onRolesChange}
|
||||
placeholder="Select roles"
|
||||
value={localRole}
|
||||
onChange={onRoleChange}
|
||||
placeholder="Select role"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,9 @@ import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
|
||||
import { Pagination, Skeleton } from 'antd';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
getGetServiceAccountRolesQueryKey,
|
||||
getListServiceAccountsQueryKey,
|
||||
useDeleteServiceAccountRole,
|
||||
useGetServiceAccount,
|
||||
useListServiceAccountKeys,
|
||||
useUpdateServiceAccount,
|
||||
@@ -35,7 +37,7 @@ import {
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
import { retryOn429, toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import AddKeyModal from './AddKeyModal';
|
||||
import DeleteAccountModal from './DeleteAccountModal';
|
||||
@@ -90,7 +92,7 @@ function ServiceAccountDrawer({
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
const [localName, setLocalName] = useState('');
|
||||
const [localRoles, setLocalRoles] = useState<string[]>([]);
|
||||
const [localRole, setLocalRole] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
|
||||
|
||||
@@ -138,7 +140,7 @@ function ServiceAccountDrawer({
|
||||
if (!account?.id) {
|
||||
roleSessionRef.current = null;
|
||||
} else if (account.id !== roleSessionRef.current && !isRolesLoading) {
|
||||
setLocalRoles(currentRoles.map((r) => r.id).filter(Boolean) as string[]);
|
||||
setLocalRole(currentRoles[0]?.id ?? '');
|
||||
roleSessionRef.current = account.id;
|
||||
}
|
||||
}, [account?.id, currentRoles, isRolesLoading]);
|
||||
@@ -149,13 +151,7 @@ function ServiceAccountDrawer({
|
||||
const isDirty =
|
||||
account !== null &&
|
||||
(localName !== (account.name ?? '') ||
|
||||
JSON.stringify([...localRoles].sort()) !==
|
||||
JSON.stringify(
|
||||
currentRoles
|
||||
.map((r) => r.id)
|
||||
.filter(Boolean)
|
||||
.sort(),
|
||||
));
|
||||
localRole !== (currentRoles[0]?.id ?? ''));
|
||||
|
||||
const {
|
||||
roles: availableRoles,
|
||||
@@ -183,6 +179,27 @@ function ServiceAccountDrawer({
|
||||
|
||||
// the retry for this mutation is safe due to the api being idempotent on backend
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount();
|
||||
const { mutateAsync: deleteRole } = useDeleteServiceAccountRole({
|
||||
mutation: {
|
||||
retry: retryOn429,
|
||||
},
|
||||
});
|
||||
|
||||
const executeRolesOperation = useCallback(
|
||||
async (accountId: string): Promise<RoleUpdateFailure[]> => {
|
||||
if (localRole === '' && currentRoles[0]?.id) {
|
||||
await deleteRole({
|
||||
pathParams: { id: accountId, rid: currentRoles[0].id },
|
||||
});
|
||||
await queryClient.invalidateQueries(
|
||||
getGetServiceAccountRolesQueryKey({ id: accountId }),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
return applyDiff([localRole].filter(Boolean), availableRoles);
|
||||
},
|
||||
[localRole, currentRoles, availableRoles, applyDiff, deleteRole, queryClient],
|
||||
);
|
||||
|
||||
const retryNameUpdate = useCallback(async (): Promise<void> => {
|
||||
if (!account) {
|
||||
@@ -250,7 +267,7 @@ function ServiceAccountDrawer({
|
||||
|
||||
const retryRolesUpdate = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const failures = await applyDiff([...localRoles], availableRoles);
|
||||
const failures = await executeRolesOperation(selectedAccountId ?? '');
|
||||
if (failures.length === 0) {
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Roles update'));
|
||||
} else {
|
||||
@@ -266,7 +283,7 @@ function ServiceAccountDrawer({
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [localRoles, availableRoles, applyDiff, failuresToSaveErrors]);
|
||||
}, [selectedAccountId, executeRolesOperation, failuresToSaveErrors]);
|
||||
|
||||
const handleSave = useCallback(async (): Promise<void> => {
|
||||
if (!account || !isDirty) {
|
||||
@@ -285,7 +302,7 @@ function ServiceAccountDrawer({
|
||||
|
||||
const [nameResult, rolesResult] = await Promise.allSettled([
|
||||
namePromise,
|
||||
applyDiff([...localRoles], availableRoles),
|
||||
executeRolesOperation(account.id),
|
||||
]);
|
||||
|
||||
const errors: SaveError[] = [];
|
||||
@@ -326,10 +343,8 @@ function ServiceAccountDrawer({
|
||||
account,
|
||||
isDirty,
|
||||
localName,
|
||||
localRoles,
|
||||
availableRoles,
|
||||
updateMutateAsync,
|
||||
applyDiff,
|
||||
executeRolesOperation,
|
||||
refetchAccount,
|
||||
onSuccess,
|
||||
queryClient,
|
||||
@@ -428,9 +443,9 @@ function ServiceAccountDrawer({
|
||||
account={account}
|
||||
localName={localName}
|
||||
onNameChange={handleNameChange}
|
||||
localRoles={localRoles}
|
||||
onRolesChange={(roles): void => {
|
||||
setLocalRoles(roles);
|
||||
localRole={localRole}
|
||||
onRoleChange={(role): void => {
|
||||
setLocalRole(role ?? '');
|
||||
clearRoleErrors();
|
||||
}}
|
||||
isDisabled={isDeleted}
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('ServiceAccountDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('adding a role fires POST for the new role and no DELETE for existing roles', async () => {
|
||||
it('changing roles enables Save; clicking Save sends role add request without delete', async () => {
|
||||
const roleSpy = jest.fn();
|
||||
const deleteSpy = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
@@ -171,7 +171,6 @@ describe('ServiceAccountDrawer', () => {
|
||||
|
||||
await screen.findByDisplayValue('CI Bot');
|
||||
|
||||
// Add signoz-viewer while keeping signoz-admin selected
|
||||
await user.click(screen.getByLabelText('Roles'));
|
||||
await user.click(await screen.findByTitle('signoz-viewer'));
|
||||
|
||||
@@ -189,43 +188,6 @@ describe('ServiceAccountDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('removing a role fires DELETE for the removed role and no POST', async () => {
|
||||
const roleSpy = jest.fn();
|
||||
const deleteSpy = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(SA_ROLES_ENDPOINT, async (req, res, ctx) => {
|
||||
roleSpy(await req.json());
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) => {
|
||||
deleteSpy();
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
);
|
||||
|
||||
renderDrawer();
|
||||
|
||||
await screen.findByDisplayValue('CI Bot');
|
||||
|
||||
// Remove the signoz-admin tag from the multi-select
|
||||
const adminTag = await screen.findByTitle('signoz-admin');
|
||||
const removeBtn = adminTag.querySelector(
|
||||
'.ant-select-selection-item-remove',
|
||||
) as Element;
|
||||
await user.click(removeBtn);
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /Save Changes/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteSpy).toHaveBeenCalled();
|
||||
expect(roleSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('"Delete Service Account" opens confirm dialog; confirming sends delete request', async () => {
|
||||
const deleteSpy = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CSSProperties } from 'react';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, SpinProps } from 'antd';
|
||||
|
||||
import { SpinerStyle } from './styles';
|
||||
@@ -7,14 +7,7 @@ import { SpinerStyle } from './styles';
|
||||
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
|
||||
return (
|
||||
<SpinerStyle height={height} style={style}>
|
||||
<Spin
|
||||
spinning
|
||||
size={size}
|
||||
tip={tip}
|
||||
indicator={
|
||||
<Loader className="animate-spin" role="img" aria-label="loading" />
|
||||
}
|
||||
/>
|
||||
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
|
||||
</SpinerStyle>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'react';
|
||||
import type { TableComponents } from 'react-virtuoso';
|
||||
import { TableVirtuoso, TableVirtuosoHandle } from 'react-virtuoso';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { DndContext, pointerWithin } from '@dnd-kit/core';
|
||||
import {
|
||||
horizontalListSortingStrategy,
|
||||
@@ -580,10 +580,7 @@ function TanStackTableInner<TData>(
|
||||
className={viewStyles.tanstackLoadingOverlay}
|
||||
data-testid="tanstack-infinite-loader"
|
||||
>
|
||||
<Spin
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
tip="Loading more..."
|
||||
/>
|
||||
<Spin indicator={<LoadingOutlined spin />} tip="Loading more..." />
|
||||
</div>
|
||||
)}
|
||||
{showPagination && pagination && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { QueryFunctionContext, useQueries, useQuery } from 'react-query';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Switch, Table, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { getQueryRangeV5 } from 'api/v5/queryRange/getQueryRange';
|
||||
@@ -202,9 +202,7 @@ function TopErrors({
|
||||
columns={topErrorsColumnsConfig}
|
||||
loading={{
|
||||
spinning: isLoading || isRefetching,
|
||||
indicator: (
|
||||
<Spin indicator={<Loader size={14} className="animate-spin" />} />
|
||||
),
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
dataSource={isLoading || isRefetching ? [] : formattedTopErrorsData}
|
||||
locale={{
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Table } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import emptyStateUrl from 'assets/Icons/emptyState.svg';
|
||||
@@ -205,9 +205,7 @@ function DomainList(): JSX.Element {
|
||||
columns={columnsConfig}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: (
|
||||
<Spin indicator={<Loader size={14} className="animate-spin" />} />
|
||||
),
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useInterval } from 'react-use';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { Compass, Loader, ScrollText } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Compass, ScrollText } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Modal, Spin } from 'antd';
|
||||
import setRetentionApi from 'api/settings/setRetention';
|
||||
import setRetentionApiV2 from 'api/settings/setRetentionV2';
|
||||
@@ -479,11 +480,7 @@ function GeneralSettings({
|
||||
saveButtonText:
|
||||
metricsTtlValuesPayload.status === 'pending' ? (
|
||||
<span>
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>{' '}
|
||||
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
|
||||
{t('retention_save_button.pending', { name: 'metrics' })}
|
||||
</span>
|
||||
) : (
|
||||
@@ -526,11 +523,7 @@ function GeneralSettings({
|
||||
saveButtonText:
|
||||
tracesTtlValuesPayload.status === 'pending' ? (
|
||||
<span>
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>{' '}
|
||||
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
|
||||
{t('retention_save_button.pending', { name: 'traces' })}
|
||||
</span>
|
||||
) : (
|
||||
@@ -572,11 +565,7 @@ function GeneralSettings({
|
||||
saveButtonText:
|
||||
logsTtlValuesPayload.status === 'pending' ? (
|
||||
<span>
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>{' '}
|
||||
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
|
||||
{t('retention_save_button.pending', { name: 'logs' })}
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -9,8 +9,11 @@ import React, {
|
||||
import { useQueryClient } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
|
||||
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import {
|
||||
LoadingOutlined,
|
||||
SearchOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input, Spin } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
@@ -335,7 +338,7 @@ function FullView({
|
||||
)}
|
||||
<div className="time-container">
|
||||
{response.isFetching && (
|
||||
<Spin spinning indicator={<Loader className="animate-spin" />} />
|
||||
<Spin spinning indicator={<LoadingOutlined spin />} />
|
||||
)}
|
||||
<TimePreference
|
||||
selectedTime={selectedTime}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoaderCircle } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -79,7 +79,7 @@ export function RequestDashboardBtn(): JSX.Element {
|
||||
className="periscope-btn primary"
|
||||
icon={
|
||||
isSubmittingRequestForDashboard ? (
|
||||
<LoaderCircle className="animate-spin" size={12} />
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<Check size={12} />
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { LoaderCircle } from '@signozhq/icons';
|
||||
import { Spin } from 'antd';
|
||||
import { CircleCheck } from 'lucide-react';
|
||||
|
||||
@@ -21,13 +21,7 @@ export default function QueryStatus(
|
||||
|
||||
const content = useMemo((): React.ReactElement => {
|
||||
if (loading) {
|
||||
return (
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<LoaderCircle className="animate-spin" />}
|
||||
/>
|
||||
);
|
||||
return <Spin spinning size="small" indicator={<LoadingOutlined spin />} />;
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Popover, Spin } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -32,13 +32,7 @@ function FieldItem({
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>
|
||||
);
|
||||
return <Spin spinning size="small" indicator={<LoadingOutlined spin />} />;
|
||||
}
|
||||
|
||||
if (isHovered) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Table, TablePaginationConfig, TableProps, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
@@ -79,7 +79,7 @@ function MetricsTable({
|
||||
indicator: (
|
||||
<Spin
|
||||
data-testid="metrics-table-loading-state"
|
||||
indicator={<Loader size={14} className="animate-spin" />}
|
||||
indicator={<LoadingOutlined size={14} spin />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons';
|
||||
import { LoaderCircle } from '@signozhq/icons';
|
||||
import {
|
||||
CheckCircleTwoTone,
|
||||
CloseCircleTwoTone,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import MessagingQueueHealthCheck from 'components/MessagingQueueHealthCheck/MessagingQueueHealthCheck';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -338,7 +341,7 @@ export default function ConnectionStatus(): JSX.Element {
|
||||
<div className="label"> Status </div>
|
||||
|
||||
<div className="status">
|
||||
{isQueryServiceLoading && <LoaderCircle className="animate-spin" />}
|
||||
{isQueryServiceLoading && <LoadingOutlined />}
|
||||
{!isQueryServiceLoading &&
|
||||
isReceivingData &&
|
||||
(getStartedSource !== 'kafka' ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoaderCircle } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Form, Input, Select, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -272,7 +272,7 @@ export default function DataSource(): JSX.Element {
|
||||
className="periscope-btn primary"
|
||||
icon={
|
||||
isSubmittingRequestForDataSource ? (
|
||||
<LoaderCircle className="animate-spin" />
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<Check size={12} />
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoaderCircle } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Form, Input, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -185,7 +185,7 @@ export default function EnvironmentDetails(): JSX.Element {
|
||||
className="periscope-btn primary"
|
||||
icon={
|
||||
isSubmittingRequestForEnvironment ? (
|
||||
<LoaderCircle className="animate-spin" />
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<Check size={12} />
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons';
|
||||
import { LoaderCircle } from '@signozhq/icons';
|
||||
import {
|
||||
CheckCircleTwoTone,
|
||||
CloseCircleTwoTone,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -242,7 +245,7 @@ export default function LogsConnectionStatus(): JSX.Element {
|
||||
<div className="label"> Status </div>
|
||||
|
||||
<div className="status">
|
||||
{(loading || isFetching) && <LoaderCircle className="animate-spin" />}
|
||||
{(loading || isFetching) && <LoadingOutlined />}
|
||||
{!(loading || isFetching) && isReceivingData && (
|
||||
<>
|
||||
<CheckCircleTwoTone twoToneColor="#52c41a" />
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
CheckCircleFilled,
|
||||
CloseCircleFilled,
|
||||
ExclamationCircleFilled,
|
||||
LoadingOutlined,
|
||||
MinusCircleFilled,
|
||||
} from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Spin } from 'antd';
|
||||
|
||||
export function getDeploymentStage(value: string): string {
|
||||
@@ -28,17 +28,7 @@ export function getDeploymentStageIcon(value: string): JSX.Element {
|
||||
switch (value) {
|
||||
case 'in_progress':
|
||||
return (
|
||||
<Spin
|
||||
indicator={
|
||||
<Loader
|
||||
size="large"
|
||||
className="animate-spin"
|
||||
role="img"
|
||||
aria-label="loading"
|
||||
data-icon="loading"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 15 }} spin />} />
|
||||
);
|
||||
case 'deployed':
|
||||
return <CheckCircleFilled />;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { LinkOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { LinkOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -231,7 +230,7 @@ const useBaseAggregateOptions = ({
|
||||
key={key}
|
||||
icon={
|
||||
isLoading ? (
|
||||
<Loader className="animate-spin" />
|
||||
<LoadingOutlined spin />
|
||||
) : (
|
||||
<span style={{ color: aggregateData?.seriesColor }}>{icon}</span>
|
||||
)
|
||||
|
||||
@@ -29,18 +29,6 @@
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
&__close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
color: var(--foreground);
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__header-divider {
|
||||
display: block;
|
||||
width: 1px;
|
||||
|
||||
@@ -78,39 +78,50 @@ function ResourceRow({
|
||||
<RadioGroupLabel htmlFor={`${resource.id}-all`}>All</RadioGroupLabel>
|
||||
</div>
|
||||
|
||||
<div className="psp-resource__radio-item">
|
||||
<RadioGroupItem
|
||||
value={PermissionScope.ONLY_SELECTED}
|
||||
id={`${resource.id}-only-selected`}
|
||||
/>
|
||||
<RadioGroupLabel htmlFor={`${resource.id}-only-selected`}>
|
||||
Only selected
|
||||
</RadioGroupLabel>
|
||||
</div>
|
||||
{resource.type === 'metaresources' ? (
|
||||
<div className="psp-resource__radio-item">
|
||||
<RadioGroupItem
|
||||
value={PermissionScope.ONLY_SELECTED}
|
||||
id={`${resource.id}-none`}
|
||||
/>
|
||||
<RadioGroupLabel htmlFor={`${resource.id}-none`}>None</RadioGroupLabel>
|
||||
</div>
|
||||
) : (
|
||||
<div className="psp-resource__radio-item">
|
||||
<RadioGroupItem
|
||||
value={PermissionScope.ONLY_SELECTED}
|
||||
id={`${resource.id}-only-selected`}
|
||||
/>
|
||||
<RadioGroupLabel htmlFor={`${resource.id}-only-selected`}>
|
||||
Only selected
|
||||
</RadioGroupLabel>
|
||||
</div>
|
||||
)}
|
||||
</RadioGroup>
|
||||
|
||||
{config.scope === PermissionScope.ONLY_SELECTED && (
|
||||
<div className="psp-resource__select-wrapper">
|
||||
{/* TODO: right now made to only accept user input, we need to give it proper resource based value fetching from APIs */}
|
||||
<Select
|
||||
mode="tags"
|
||||
value={config.selectedIds}
|
||||
onChange={(vals: string[]): void =>
|
||||
onSelectedIdsChange(resource.id, vals)
|
||||
}
|
||||
options={resource.options ?? []}
|
||||
placeholder="Select resources..."
|
||||
className="psp-resource__select"
|
||||
popupClassName="psp-resource__select-popup"
|
||||
showSearch
|
||||
filterOption={(input, option): boolean =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{config.scope === PermissionScope.ONLY_SELECTED &&
|
||||
resource.type !== 'metaresources' && (
|
||||
<div className="psp-resource__select-wrapper">
|
||||
{/* TODO: right now made to only accept user input, we need to give it proper resource based value fetching from APIs */}
|
||||
<Select
|
||||
mode="tags"
|
||||
value={config.selectedIds}
|
||||
onChange={(vals: string[]): void =>
|
||||
onSelectedIdsChange(resource.id, vals)
|
||||
}
|
||||
options={resource.options ?? []}
|
||||
placeholder="Select resources..."
|
||||
className="psp-resource__select"
|
||||
popupClassName="psp-resource__select-popup"
|
||||
showSearch
|
||||
filterOption={(input, option): boolean =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -213,13 +224,13 @@ function PermissionSidePanel({
|
||||
<div className="permission-side-panel">
|
||||
<div className="permission-side-panel__header">
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className="permission-side-panel__close"
|
||||
onClick={onClose}
|
||||
aria-label="Close panel"
|
||||
>
|
||||
<X size={16} />
|
||||
<X size={14} />
|
||||
</Button>
|
||||
<span className="permission-side-panel__header-divider" />
|
||||
<span className="permission-side-panel__title">
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface ResourceOption {
|
||||
|
||||
export interface ResourceDefinition {
|
||||
id: string;
|
||||
kind: string;
|
||||
type: string;
|
||||
label: string;
|
||||
options?: ResourceOption[];
|
||||
}
|
||||
|
||||
@@ -282,30 +282,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-delete-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
color: var(--destructive);
|
||||
opacity: 0.6;
|
||||
padding: 0;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
opacity 0.2s;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.role-details-delete-modal {
|
||||
width: calc(100% - 30px) !important;
|
||||
max-width: 384px;
|
||||
|
||||
@@ -110,7 +110,6 @@ function RoleDetailsPage(): JSX.Element {
|
||||
getGetObjectsQueryKey({ id: roleId, relation: activePermission }),
|
||||
);
|
||||
}
|
||||
setActivePermission(null);
|
||||
};
|
||||
|
||||
const { mutate: patchObjects, isLoading: isSaving } = usePatchObjects({
|
||||
@@ -213,18 +212,16 @@ function RoleDetailsPage(): JSX.Element {
|
||||
{!isManaged && (
|
||||
<div className="role-details-actions">
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="link"
|
||||
color="destructive"
|
||||
className="role-details-delete-action-btn"
|
||||
onClick={(): void => setIsDeleteModalOpen(true)}
|
||||
aria-label="Delete role"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={(): void => setIsEditModalOpen(true)}
|
||||
>
|
||||
Edit Role Details
|
||||
|
||||
@@ -27,9 +27,8 @@ function DeleteRoleModal({
|
||||
<Button
|
||||
key="cancel"
|
||||
className="cancel-btn"
|
||||
prefix={<X size={16} />}
|
||||
prefix={<X size={14} />}
|
||||
onClick={onCancel}
|
||||
size="sm"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
>
|
||||
@@ -38,10 +37,9 @@ function DeleteRoleModal({
|
||||
<Button
|
||||
key="delete"
|
||||
className="delete-btn"
|
||||
prefix={<Trash2 size={16} />}
|
||||
prefix={<Trash2 size={14} />}
|
||||
onClick={onConfirm}
|
||||
loading={isDeleting}
|
||||
size="sm"
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
>
|
||||
|
||||
@@ -285,12 +285,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
// todo: https://github.com/SigNoz/components/issues/116
|
||||
input,
|
||||
input {
|
||||
&::placeholder {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
box-sizing: border-box;
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
background: var(--input-background, transparent);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
padding: 6px 8px;
|
||||
font-family: Inter;
|
||||
@@ -303,7 +310,7 @@
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--muted-foreground);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
@@ -313,25 +320,6 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
|
||||
@@ -58,8 +58,18 @@ const baseAuthzResources: AuthzResources = {
|
||||
};
|
||||
|
||||
const resourceDefs: ResourceDefinition[] = [
|
||||
{ id: 'dashboard', label: 'Dashboard' },
|
||||
{ id: 'alert', label: 'Alert' },
|
||||
{
|
||||
id: 'metaresource:dashboard',
|
||||
kind: 'dashboard',
|
||||
type: 'metaresource',
|
||||
label: 'Dashboard',
|
||||
},
|
||||
{
|
||||
id: 'metaresource:alert',
|
||||
kind: 'alert',
|
||||
type: 'metaresource',
|
||||
label: 'Alert',
|
||||
},
|
||||
];
|
||||
|
||||
const ID_A = 'aaaaaaaa-0000-0000-0000-000000000001';
|
||||
@@ -69,15 +79,24 @@ const ID_C = 'cccccccc-0000-0000-0000-000000000003';
|
||||
describe('buildPatchPayload', () => {
|
||||
it('sends only the added selector as an addition', () => {
|
||||
const initial: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [ID_A] },
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A],
|
||||
},
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
const newConfig: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A, ID_B],
|
||||
},
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildPatchPayload({
|
||||
@@ -95,18 +114,24 @@ describe('buildPatchPayload', () => {
|
||||
|
||||
it('sends only the removed selector as a deletion', () => {
|
||||
const initial: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A, ID_B, ID_C],
|
||||
},
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
const newConfig: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A, ID_C],
|
||||
},
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildPatchPayload({
|
||||
@@ -124,18 +149,24 @@ describe('buildPatchPayload', () => {
|
||||
|
||||
it('treats selector order as irrelevant — produces no payload when IDs are identical', () => {
|
||||
const initial: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A, ID_B],
|
||||
},
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
const newConfig: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_B, ID_A],
|
||||
},
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildPatchPayload({
|
||||
@@ -151,15 +182,21 @@ describe('buildPatchPayload', () => {
|
||||
|
||||
it('replaces wildcard with specific IDs when switching all → only_selected', () => {
|
||||
const initial: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
const newConfig: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A, ID_B],
|
||||
},
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildPatchPayload({
|
||||
@@ -179,12 +216,21 @@ describe('buildPatchPayload', () => {
|
||||
|
||||
it('only deletes wildcard when switching all → only_selected with empty selector list', () => {
|
||||
const initial: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
const newConfig: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildPatchPayload({
|
||||
@@ -202,12 +248,18 @@ describe('buildPatchPayload', () => {
|
||||
|
||||
it('only includes resources that actually changed', () => {
|
||||
const initial: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [ID_A] },
|
||||
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A],
|
||||
},
|
||||
};
|
||||
const newConfig: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ALL, selectedIds: [] }, // unchanged
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [ID_A, ID_B] }, // added ID_B
|
||||
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] }, // unchanged
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A, ID_B],
|
||||
}, // added ID_B
|
||||
};
|
||||
|
||||
const result = buildPatchPayload({
|
||||
@@ -232,7 +284,7 @@ describe('objectsToPermissionConfig', () => {
|
||||
|
||||
const result = objectsToPermissionConfig(objects, resourceDefs);
|
||||
|
||||
expect(result.dashboard).toStrictEqual({
|
||||
expect(result['metaresource:dashboard']).toStrictEqual({
|
||||
scope: PermissionScope.ALL,
|
||||
selectedIds: [],
|
||||
});
|
||||
@@ -245,7 +297,7 @@ describe('objectsToPermissionConfig', () => {
|
||||
|
||||
const result = objectsToPermissionConfig(objects, resourceDefs);
|
||||
|
||||
expect(result.dashboard).toStrictEqual({
|
||||
expect(result['metaresource:dashboard']).toStrictEqual({
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A, ID_B],
|
||||
});
|
||||
@@ -254,11 +306,11 @@ describe('objectsToPermissionConfig', () => {
|
||||
it('defaults to ONLY_SELECTED with empty selectedIds when resource is absent from API response', () => {
|
||||
const result = objectsToPermissionConfig([], resourceDefs);
|
||||
|
||||
expect(result.dashboard).toStrictEqual({
|
||||
expect(result['metaresource:dashboard']).toStrictEqual({
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
});
|
||||
expect(result.alert).toStrictEqual({
|
||||
expect(result['metaresource:alert']).toStrictEqual({
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
});
|
||||
@@ -268,8 +320,11 @@ describe('objectsToPermissionConfig', () => {
|
||||
describe('configsEqual', () => {
|
||||
it('returns true for identical configs', () => {
|
||||
const config: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [ID_A] },
|
||||
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
'metaresource:alert': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A],
|
||||
},
|
||||
};
|
||||
|
||||
expect(configsEqual(config, { ...config })).toBe(true);
|
||||
@@ -277,22 +332,25 @@ describe('configsEqual', () => {
|
||||
|
||||
it('returns false when configs differ', () => {
|
||||
const a: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
};
|
||||
const b: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
expect(configsEqual(a, b)).toBe(false);
|
||||
|
||||
const c: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_C, ID_B],
|
||||
},
|
||||
};
|
||||
const d: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A, ID_B],
|
||||
},
|
||||
@@ -303,13 +361,13 @@ describe('configsEqual', () => {
|
||||
|
||||
it('returns true when selectedIds are the same but in different order', () => {
|
||||
const a: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_A, ID_B],
|
||||
},
|
||||
};
|
||||
const b: PermissionConfig = {
|
||||
dashboard: {
|
||||
'metaresource:dashboard': {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
selectedIds: [ID_B, ID_A],
|
||||
},
|
||||
@@ -322,23 +380,25 @@ describe('configsEqual', () => {
|
||||
describe('buildConfig', () => {
|
||||
it('uses initial values when provided and defaults for resources not in initial', () => {
|
||||
const initial: PermissionConfig = {
|
||||
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
|
||||
};
|
||||
|
||||
const result = buildConfig(resourceDefs, initial);
|
||||
|
||||
expect(result.dashboard).toStrictEqual({
|
||||
expect(result['metaresource:dashboard']).toStrictEqual({
|
||||
scope: PermissionScope.ALL,
|
||||
selectedIds: [],
|
||||
});
|
||||
expect(result.alert).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
|
||||
expect(result['metaresource:alert']).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
|
||||
});
|
||||
|
||||
it('applies DEFAULT_RESOURCE_CONFIG to all resources when no initial is provided', () => {
|
||||
const result = buildConfig(resourceDefs);
|
||||
|
||||
expect(result.dashboard).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
|
||||
expect(result.alert).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
|
||||
expect(result['metaresource:dashboard']).toStrictEqual(
|
||||
DEFAULT_RESOURCE_CONFIG,
|
||||
);
|
||||
expect(result['metaresource:alert']).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -375,7 +435,10 @@ describe('deriveResourcesForRelation', () => {
|
||||
const result = deriveResourcesForRelation(baseAuthzResources, 'create');
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((r) => r.id)).toStrictEqual(['dashboard', 'alert']);
|
||||
expect(result.map((r) => r.id)).toStrictEqual([
|
||||
'metaresource:dashboard',
|
||||
'metaresource:alert',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns an empty array when authzResources is null', () => {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const IS_ROLE_DETAILS_AND_CRUD_ENABLED = false;
|
||||
export const IS_ROLE_DETAILS_AND_CRUD_ENABLED = true;
|
||||
|
||||
@@ -70,7 +70,9 @@ export function deriveResourcesForRelation(
|
||||
return authzResources.resources
|
||||
.filter((r) => supportedTypes.includes(r.type))
|
||||
.map((r) => ({
|
||||
id: r.kind,
|
||||
id: `${r.type}:${r.kind}`,
|
||||
kind: r.kind,
|
||||
type: r.type,
|
||||
label: capitalize(r.kind).replaceAll('_', ' '),
|
||||
options: [],
|
||||
}));
|
||||
@@ -82,7 +84,9 @@ export function objectsToPermissionConfig(
|
||||
): PermissionConfig {
|
||||
const config: PermissionConfig = {};
|
||||
for (const res of resources) {
|
||||
const obj = objects.find((o) => o.resource.kind === res.id);
|
||||
const obj = objects.find(
|
||||
(o) => o.resource.kind === res.kind && o.resource.type === res.type,
|
||||
);
|
||||
if (!obj) {
|
||||
config[res.id] = {
|
||||
scope: PermissionScope.ONLY_SELECTED,
|
||||
@@ -118,7 +122,9 @@ export function buildPatchPayload({
|
||||
for (const res of resources) {
|
||||
const initial = initialConfig[res.id];
|
||||
const current = newConfig[res.id];
|
||||
const resourceDef = authzRes.resources.find((r) => r.kind === res.id);
|
||||
const resourceDef = authzRes.resources.find(
|
||||
(r) => r.kind === res.kind && r.type === res.type,
|
||||
);
|
||||
if (!resourceDef) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Spin } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
@@ -215,7 +215,7 @@ function AddSpanToFunnelModal({
|
||||
<Spin
|
||||
className="add-span-to-funnel-modal__loading-spinner"
|
||||
spinning={isFunnelDetailsLoading || isFunnelDetailsFetching}
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
indicator={<LoadingOutlined spin />}
|
||||
>
|
||||
{selectedFunnelId && funnelDetails?.payload && (
|
||||
<FunnelProvider
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Spin, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AxiosError } from 'axios';
|
||||
@@ -189,9 +188,7 @@ function Filters({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isFetching && (
|
||||
<Spin indicator={<Loader className="animate-spin" />} size="small" />
|
||||
)}
|
||||
{isFetching && <Spin indicator={<LoadingOutlined spin />} size="small" />}
|
||||
{error && (
|
||||
<Tooltip title={(error as AxiosError)?.message || 'Something went wrong'}>
|
||||
<InfoCircleOutlined size={14} />
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useQueryClient } from 'react-query';
|
||||
import {
|
||||
getGetServiceAccountRolesQueryKey,
|
||||
useCreateServiceAccountRole,
|
||||
useDeleteServiceAccountRole,
|
||||
useGetServiceAccountRoles,
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
@@ -45,9 +44,6 @@ export function useServiceAccountRoleManager(
|
||||
const { mutateAsync: createRole } = useCreateServiceAccountRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
const { mutateAsync: deleteRole } = useDeleteServiceAccountRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
|
||||
const invalidateRoles = useCallback(
|
||||
() =>
|
||||
@@ -72,21 +68,14 @@ export function useServiceAccountRoleManager(
|
||||
const addedRoles = availableRoles.filter(
|
||||
(r) => r.id && desiredRoleIds.has(r.id) && !currentRoleIds.has(r.id),
|
||||
);
|
||||
const removedRoles = currentRoles.filter(
|
||||
(r) => r.id && !desiredRoleIds.has(r.id),
|
||||
);
|
||||
|
||||
// TODO: re-enable deletes once BE for this is streamlined
|
||||
const allOperations = [
|
||||
...addedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof createRole> =>
|
||||
createRole({ pathParams: { id: accountId }, data: { id: role.id } }),
|
||||
})),
|
||||
...removedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof deleteRole> =>
|
||||
deleteRole({ pathParams: { id: accountId, rid: role.id ?? '' } }),
|
||||
})),
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
@@ -117,7 +106,7 @@ export function useServiceAccountRoleManager(
|
||||
|
||||
return failures;
|
||||
},
|
||||
[accountId, currentRoles, createRole, deleteRole, invalidateRoles],
|
||||
[accountId, currentRoles, createRole, invalidateRoles],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('computeVisualLayout', () => {
|
||||
it('should handle empty input', () => {
|
||||
const layout = computeVisualLayout([]);
|
||||
expect(layout.totalVisualRows).toBe(0);
|
||||
expect(layout.visualRows).toStrictEqual([]);
|
||||
expect(layout.visualRows).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle single root, no children — 1 visual row', () => {
|
||||
@@ -36,7 +36,7 @@ describe('computeVisualLayout', () => {
|
||||
});
|
||||
const layout = computeVisualLayout([[root]]);
|
||||
expect(layout.totalVisualRows).toBe(1);
|
||||
expect(layout.visualRows[0]).toStrictEqual([root]);
|
||||
expect(layout.visualRows[0]).toEqual([root]);
|
||||
expect(layout.spanToVisualRow['root']).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -44,12 +44,12 @@ function makeSpan(
|
||||
|
||||
describe('getVisibleSpans', () => {
|
||||
it('returns empty array for empty input', () => {
|
||||
expect(getVisibleSpans([], new Set())).toStrictEqual([]);
|
||||
expect(getVisibleSpans([], new Set())).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns single root span with no children', () => {
|
||||
const spans = [makeSpan({ span_id: 'root', level: 0 })];
|
||||
expect(getVisibleSpans(spans, new Set())).toStrictEqual(spans);
|
||||
expect(getVisibleSpans(spans, new Set())).toEqual(spans);
|
||||
});
|
||||
|
||||
it('returns all spans for flat tree (all level 0, no children)', () => {
|
||||
@@ -58,7 +58,7 @@ describe('getVisibleSpans', () => {
|
||||
makeSpan({ span_id: 'b', level: 0 }),
|
||||
makeSpan({ span_id: 'c', level: 0 }),
|
||||
];
|
||||
expect(getVisibleSpans(spans, new Set())).toStrictEqual(spans);
|
||||
expect(getVisibleSpans(spans, new Set())).toEqual(spans);
|
||||
});
|
||||
|
||||
it('returns all spans when all parents are expanded', () => {
|
||||
@@ -68,7 +68,7 @@ describe('getVisibleSpans', () => {
|
||||
makeSpan({ span_id: 'b', level: 1 }),
|
||||
];
|
||||
const uncollapsed = new Set(['root']);
|
||||
expect(getVisibleSpans(spans, uncollapsed)).toStrictEqual(spans);
|
||||
expect(getVisibleSpans(spans, uncollapsed)).toEqual(spans);
|
||||
});
|
||||
|
||||
it('hides children when root is collapsed', () => {
|
||||
@@ -99,7 +99,7 @@ describe('getVisibleSpans', () => {
|
||||
// root and A expanded, but A is collapsed
|
||||
const uncollapsed = new Set(['root']); // A not in set → collapsed
|
||||
const result = getVisibleSpans(spans, uncollapsed);
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual(['root', 'A']);
|
||||
expect(result.map((s) => s.span_id)).toEqual(['root', 'A']);
|
||||
});
|
||||
|
||||
it('collapses one subtree while sibling subtree stays visible', () => {
|
||||
@@ -116,7 +116,7 @@ describe('getVisibleSpans', () => {
|
||||
// root and childB expanded, childA collapsed
|
||||
const uncollapsed = new Set(['root', 'childB']);
|
||||
const result = getVisibleSpans(spans, uncollapsed);
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual([
|
||||
expect(result.map((s) => s.span_id)).toEqual([
|
||||
'root',
|
||||
'childA', // visible but collapsed
|
||||
'childB', // visible and expanded
|
||||
@@ -134,11 +134,11 @@ describe('getVisibleSpans', () => {
|
||||
|
||||
// Collapsed
|
||||
const collapsed = getVisibleSpans(spans, new Set());
|
||||
expect(collapsed.map((s) => s.span_id)).toStrictEqual(['root']);
|
||||
expect(collapsed.map((s) => s.span_id)).toEqual(['root']);
|
||||
|
||||
// Uncollapsed
|
||||
const uncollapsed = getVisibleSpans(spans, new Set(['root']));
|
||||
expect(uncollapsed.map((s) => s.span_id)).toStrictEqual(['root', 'a', 'b']);
|
||||
expect(uncollapsed.map((s) => s.span_id)).toEqual(['root', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('hides all levels below collapsed span in deeply nested tree', () => {
|
||||
@@ -154,7 +154,7 @@ describe('getVisibleSpans', () => {
|
||||
// Expand L0 and L1, collapse L2
|
||||
const uncollapsed = new Set(['L0', 'L1']);
|
||||
const result = getVisibleSpans(spans, uncollapsed);
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual(['L0', 'L1', 'L2']);
|
||||
expect(result.map((s) => s.span_id)).toEqual(['L0', 'L1', 'L2']);
|
||||
});
|
||||
|
||||
it('shows only root-level spans when all parents are collapsed', () => {
|
||||
@@ -166,7 +166,7 @@ describe('getVisibleSpans', () => {
|
||||
];
|
||||
const uncollapsed = new Set<string>(); // nothing expanded
|
||||
const result = getVisibleSpans(spans, uncollapsed);
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual(['root1', 'root2']);
|
||||
expect(result.map((s) => s.span_id)).toEqual(['root1', 'root2']);
|
||||
});
|
||||
|
||||
it('leaf span in uncollapsed set has no effect', () => {
|
||||
@@ -177,7 +177,7 @@ describe('getVisibleSpans', () => {
|
||||
// leaf is in uncollapsed set but has_children=false, should make no difference
|
||||
const uncollapsed = new Set(['root', 'leaf']);
|
||||
const result = getVisibleSpans(spans, uncollapsed);
|
||||
expect(result).toStrictEqual(spans);
|
||||
expect(result).toEqual(spans);
|
||||
});
|
||||
|
||||
it('handles 10k spans within 50ms', () => {
|
||||
@@ -218,13 +218,7 @@ describe('getVisibleSpans', () => {
|
||||
// root and C expanded; A and B collapsed
|
||||
const uncollapsed = new Set(['root', 'C']);
|
||||
const result = getVisibleSpans(spans, uncollapsed);
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual([
|
||||
'root',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'C1',
|
||||
]);
|
||||
expect(result.map((s) => s.span_id)).toEqual(['root', 'A', 'B', 'C', 'C1']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -240,7 +234,7 @@ describe('getAncestorSpanIds', () => {
|
||||
makeSpan({ span_id: 'child', level: 1, parent_span_id: 'root' }),
|
||||
];
|
||||
const ancestors = getAncestorSpanIds(spans, 'child');
|
||||
expect(ancestors).toStrictEqual(new Set(['root']));
|
||||
expect(ancestors).toEqual(new Set(['root']));
|
||||
});
|
||||
|
||||
it('returns all ancestors for deeply nested span', () => {
|
||||
@@ -261,7 +255,7 @@ describe('getAncestorSpanIds', () => {
|
||||
makeSpan({ span_id: 'L3', level: 3, parent_span_id: 'L2' }),
|
||||
];
|
||||
const ancestors = getAncestorSpanIds(spans, 'L3');
|
||||
expect(ancestors).toStrictEqual(new Set(['L2', 'L1', 'L0']));
|
||||
expect(ancestors).toEqual(new Set(['L2', 'L1', 'L0']));
|
||||
});
|
||||
|
||||
it('returns empty set for unknown span', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Empty, Spin } from 'antd';
|
||||
import {
|
||||
BarController,
|
||||
@@ -97,7 +97,7 @@ function FunnelGraph(): JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<Spin spinning={isFetching} indicator={<Loader className="animate-spin" />}>
|
||||
<Spin spinning={isFetching} indicator={<LoadingOutlined spin />}>
|
||||
<div className={cx('funnel-graph', `funnel-graph--${totalSteps}-columns`)}>
|
||||
<div className="funnel-graph__chart-container">
|
||||
<canvas ref={canvasRef} />
|
||||
|
||||
@@ -54,23 +54,9 @@ describe('globalTimeStore', () => {
|
||||
expect(result.current.lastRefreshTimestamp).toBe(0);
|
||||
});
|
||||
|
||||
it('should have lastComputedMinMax computed from initial selectedTime', () => {
|
||||
it('should have lastComputedMinMax with default values', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
// Now computes min/max on store creation, no longer starts with 0
|
||||
expect(result.current.lastComputedMinMax.minTime).toBeGreaterThan(0);
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not crash with invalid relative time format', () => {
|
||||
// Invalid time formats should not crash store creation
|
||||
const store = createGlobalTimeStore({
|
||||
selectedTime: 'invalid_time' as GlobalTimeSelectedTime,
|
||||
});
|
||||
|
||||
// Store should be created successfully
|
||||
expect(store.getState().selectedTime).toBe('invalid_time');
|
||||
// Should fallback to {0, 0} when parsing fails
|
||||
expect(store.getState().lastComputedMinMax).toStrictEqual({
|
||||
expect(result.current.lastComputedMinMax).toStrictEqual({
|
||||
minTime: 0,
|
||||
maxTime: 0,
|
||||
});
|
||||
@@ -738,8 +724,8 @@ describe('globalTimeStore', () => {
|
||||
const wrapper = createIsolatedWrapper();
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Initial state now has computed values (no longer 0)
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBeGreaterThan(0);
|
||||
// Initial state has 0 values
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBe(0);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
|
||||
@@ -134,17 +134,17 @@ describe('useComputedMinMaxSync', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should have computed min/max on store creation (no longer needs mount sync)', () => {
|
||||
it('should compute min/max on mount when store has zero values', () => {
|
||||
const contextStore = createGlobalTimeStore({ selectedTime: '15m' });
|
||||
|
||||
// Store now computes min/max on creation, not on mount
|
||||
expect(contextStore.getState().lastComputedMinMax.minTime).toBeGreaterThan(0);
|
||||
expect(contextStore.getState().lastComputedMinMax.maxTime).toBeGreaterThan(0);
|
||||
expect(contextStore.getState().lastComputedMinMax).toStrictEqual({
|
||||
minTime: 0,
|
||||
maxTime: 0,
|
||||
});
|
||||
|
||||
// Hook still works but is a no-op when values already exist
|
||||
renderHook(() => useComputedMinMaxSync(contextStore));
|
||||
|
||||
// Values remain computed
|
||||
// Should have computed values now
|
||||
expect(contextStore.getState().lastComputedMinMax.maxTime).toBeGreaterThan(0);
|
||||
expect(contextStore.getState().lastComputedMinMax.minTime).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
computeRounded5sMinMax,
|
||||
isCustomTimeRange,
|
||||
parseSelectedTime,
|
||||
safeParseSelectedTime,
|
||||
} from './utils';
|
||||
|
||||
export type GlobalTimeStoreApi = StoreApi<GlobalTimeStore>;
|
||||
@@ -41,7 +40,7 @@ export function createGlobalTimeStore(
|
||||
refreshInterval,
|
||||
isRefreshEnabled: computeIsRefreshEnabled(selectedTime, refreshInterval),
|
||||
lastRefreshTimestamp: 0,
|
||||
lastComputedMinMax: safeParseSelectedTime(selectedTime),
|
||||
lastComputedMinMax: { minTime: 0, maxTime: 0 },
|
||||
|
||||
setSelectedTime: (
|
||||
time: GlobalTimeSelectedTime,
|
||||
|
||||
@@ -61,8 +61,6 @@ const fallbackDurationInNanoSeconds = 30 * 1000 * NANO_SECOND_MULTIPLIER; // 30s
|
||||
* Parse the selectedTime string to get min/max time values.
|
||||
* For relative times, computes fresh values based on Date.now().
|
||||
* For custom times, extracts the stored min/max values.
|
||||
*
|
||||
* @throws Error - When selectedTime is relativeTime and it's invalid
|
||||
*/
|
||||
export function parseSelectedTime(selectedTime: string): ParsedTimeRange {
|
||||
if (isCustomTimeRange(selectedTime)) {
|
||||
@@ -80,18 +78,6 @@ export function parseSelectedTime(selectedTime: string): ParsedTimeRange {
|
||||
return getMinMaxForSelectedTime(selectedTime as Time, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@ref parseSelectedTime} can throw errors, this handles and fallbacks to 0,0 if invalid selected time is provided
|
||||
*/
|
||||
export function safeParseSelectedTime(selectedTime: string): ParsedTimeRange {
|
||||
try {
|
||||
return parseSelectedTime(selectedTime);
|
||||
} catch (e) {
|
||||
console.error('Error parsing selected time:', e);
|
||||
return { minTime: 0, maxTime: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use store.getAutoRefreshQueryKey() instead.
|
||||
* Access via: const getAutoRefreshQueryKey = useGlobalTime((s) => s.getAutoRefreshQueryKey);
|
||||
|
||||
@@ -86,24 +86,5 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/infra_monitoring/clusters", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListClusters),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListClusters",
|
||||
Tags: []string{"inframonitoring"},
|
||||
Summary: "List Clusters for Infra Monitoring",
|
||||
Description: "Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.",
|
||||
Request: new(inframonitoringtypes.PostableClusters),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(inframonitoringtypes.Clusters),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// buildClusterRecords assembles the page records. Node condition counts and
|
||||
// pod phase counts come from the respective per-group maps in both modes;
|
||||
// every row is a group of nodes+pods, so there's no per-row "current state"
|
||||
// concept (analogous to namespaces).
|
||||
func buildClusterRecords(
|
||||
resp *qbtypes.QueryRangeResponse,
|
||||
pageGroups []map[string]string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
metadataMap map[string]map[string]string,
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts,
|
||||
podPhaseCountsMap map[string]podPhaseCounts,
|
||||
) []inframonitoringtypes.ClusterRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
records := make([]inframonitoringtypes.ClusterRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
clusterName := labels[clusterNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.ClusterRecord{ // initialize with default values
|
||||
ClusterName: clusterName,
|
||||
ClusterCPU: -1,
|
||||
ClusterCPUAllocatable: -1,
|
||||
ClusterMemory: -1,
|
||||
ClusterMemoryAllocatable: -1,
|
||||
Meta: map[string]string{},
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
if v, exists := metrics["A"]; exists {
|
||||
record.ClusterCPU = v
|
||||
}
|
||||
if v, exists := metrics["B"]; exists {
|
||||
record.ClusterCPUAllocatable = v
|
||||
}
|
||||
if v, exists := metrics["C"]; exists {
|
||||
record.ClusterMemory = v
|
||||
}
|
||||
if v, exists := metrics["D"]; exists {
|
||||
record.ClusterMemoryAllocatable = v
|
||||
}
|
||||
}
|
||||
|
||||
if conditionCountsForGroup, ok := nodeConditionCountsMap[compositeKey]; ok {
|
||||
record.NodeCountsByReadiness = inframonitoringtypes.NodeCountsByReadiness{
|
||||
Ready: conditionCountsForGroup.Ready,
|
||||
NotReady: conditionCountsForGroup.NotReady,
|
||||
}
|
||||
}
|
||||
|
||||
if phaseCountsForGroup, ok := podPhaseCountsMap[compositeKey]; ok {
|
||||
record.PodCountsByPhase = inframonitoringtypes.PodCountsByPhase{
|
||||
Pending: phaseCountsForGroup.Pending,
|
||||
Running: phaseCountsForGroup.Running,
|
||||
Succeeded: phaseCountsForGroup.Succeeded,
|
||||
Failed: phaseCountsForGroup.Failed,
|
||||
Unknown: phaseCountsForGroup.Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
func (m *module) getTopClusterGroups(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableClusters,
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
topReq := &qbtypes.QueryRangeRequest{
|
||||
Start: uint64(req.Start),
|
||||
End: uint64(req.End),
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: make([]qbtypes.QueryEnvelope, 0, len(queryNamesForOrderBy)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, envelope := range m.newClustersTableListQuery().CompositeQuery.Queries {
|
||||
if !slices.Contains(queryNamesForOrderBy, envelope.GetQueryName()) {
|
||||
continue
|
||||
}
|
||||
copied := envelope
|
||||
if copied.Type == qbtypes.QueryTypeBuilder {
|
||||
existingExpr := ""
|
||||
if f := copied.GetFilter(); f != nil {
|
||||
existingExpr = f.Expression
|
||||
}
|
||||
reqFilterExpr := ""
|
||||
if req.Filter != nil {
|
||||
reqFilterExpr = req.Filter.Expression
|
||||
}
|
||||
merged := mergeFilterExpressions(existingExpr, reqFilterExpr)
|
||||
copied.SetFilter(&qbtypes.Filter{Expression: merged})
|
||||
copied.SetGroupBy(req.GroupBy)
|
||||
}
|
||||
topReq.CompositeQuery.Queries = append(topReq.CompositeQuery.Queries, copied)
|
||||
}
|
||||
|
||||
resp, err := m.querier.QueryRange(ctx, orgID, topReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
func (m *module) getClustersTableMetadata(ctx context.Context, req *inframonitoringtypes.PostableClusters) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range clusterAttrKeysForMetadata {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, clustersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// TODO(nikhilmantri0902): change to k8s.cluster.uid after showing the missing
|
||||
// data banner. Carried forward from v1 (see k8sClusterUIDAttrKey in
|
||||
// pkg/query-service/app/inframetrics/clusters.go).
|
||||
const clusterNameAttrKey = "k8s.cluster.name"
|
||||
|
||||
var clusterNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: clusterNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
}
|
||||
|
||||
// clustersTableMetricNamesList drives the existence/retention check.
|
||||
// Includes k8s.node.condition_ready and k8s.pod.phase so the response
|
||||
// short-circuits cleanly when a cluster doesn't ship those metrics — even
|
||||
// though they aren't part of the QB composite query (they're queried separately
|
||||
// via getPerGroupNodeConditionCounts and getPerGroupPodPhaseCounts).
|
||||
var clustersTableMetricNamesList = []string{
|
||||
"k8s.node.cpu.usage",
|
||||
"k8s.node.allocatable_cpu",
|
||||
"k8s.node.memory.working_set",
|
||||
"k8s.node.allocatable_memory",
|
||||
"k8s.node.condition_ready", //TODO(nikhilmantri0902): should these metrics be used to count groups k8s.node.condition_ready and k8s.pod.phase
|
||||
"k8s.pod.phase",
|
||||
}
|
||||
|
||||
var clusterAttrKeysForMetadata = []string{
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
var orderByToClustersQueryNames = map[string][]string{
|
||||
inframonitoringtypes.ClustersOrderByCPU: {"A"},
|
||||
inframonitoringtypes.ClustersOrderByCPUAllocatable: {"B"},
|
||||
inframonitoringtypes.ClustersOrderByMemory: {"C"},
|
||||
inframonitoringtypes.ClustersOrderByMemoryAllocatable: {"D"},
|
||||
}
|
||||
|
||||
// newClustersTableListQuery builds the composite QB v5 request for the clusters list.
|
||||
// Cluster-scope metrics are derived by summing per-node metrics within the
|
||||
// group (default group: k8s.cluster.name). Node condition counts and pod phase
|
||||
// counts are derived separately via getPerGroupNodeConditionCounts and
|
||||
// getPerGroupPodPhaseCounts respectively (works for both list and grouped_list
|
||||
// modes), so neither is included here. Query letters A/B/C/D mirror the v1
|
||||
// implementation and the v2 nodes list.
|
||||
func (m *module) newClustersTableListQuery() *qbtypes.QueryRangeRequest {
|
||||
queries := []qbtypes.QueryEnvelope{
|
||||
// Query A: CPU usage — sum of node CPU within the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.node.cpu.usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{clusterNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query B: CPU allocatable — sum of node allocatable CPU within the group.
|
||||
// TimeAggregationLatest is the closest v5 equivalent of v1's AnyLast;
|
||||
// allocatable values change rarely so divergence in practice is negligible.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.node.allocatable_cpu",
|
||||
TimeAggregation: metrictypes.TimeAggregationLatest,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{clusterNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query C: Memory working set — sum of node memory within the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "C",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.node.memory.working_set",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{clusterNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query D: Memory allocatable — sum of node allocatable memory within the group.
|
||||
// Same Latest caveat as Query B.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "D",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.node.allocatable_memory",
|
||||
TimeAggregation: metrictypes.TimeAggregationLatest,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{clusterNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: queries,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -117,27 +117,3 @@ func (h *handler) ListNamespaces(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) ListClusters(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
var parsedReq inframonitoringtypes.PostableClusters
|
||||
if err := binding.JSON.BindBody(req.Body, &parsedReq); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.ListClusters(req.Context(), orgID, &parsedReq)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
@@ -426,105 +426,3 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableClusters) (*inframonitoringtypes.Clusters, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &inframonitoringtypes.Clusters{}
|
||||
|
||||
if req.OrderBy == nil {
|
||||
req.OrderBy = &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: inframonitoringtypes.ClustersOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []qbtypes.GroupByKey{clusterNameGroupByKey}
|
||||
resp.Type = inframonitoringtypes.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, clustersTableMetricNamesList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(missingMetrics) > 0 {
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
|
||||
resp.Records = []inframonitoringtypes.ClusterRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
if req.End < int64(minFirstReportedUnixMilli) {
|
||||
resp.EndTimeBeforeRetention = true
|
||||
resp.Records = []inframonitoringtypes.ClusterRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
|
||||
|
||||
metadataMap, err := m.getClustersTableMetadata(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
pageGroups, err := m.getTopClusterGroups(ctx, orgID, req, metadataMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
resp.Records = []inframonitoringtypes.ClusterRecord{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newClustersTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the nodes condition-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostableNodes. With default groupBy
|
||||
// [k8s.cluster.name], counts are bucketed per cluster; with a custom groupBy,
|
||||
// they aggregate across clusters in that group.
|
||||
nodeConditionCountsMap, err := m.getPerGroupNodeConditionCounts(ctx, &inframonitoringtypes.PostableNodes{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Same pattern for pod phase counts via PostablePods shim.
|
||||
podPhaseCountsMap, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildClusterRecords(queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCountsMap, podPhaseCountsMap)
|
||||
resp.Warning = queryResp.Warning
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ type Handler interface {
|
||||
ListPods(http.ResponseWriter, *http.Request)
|
||||
ListNodes(http.ResponseWriter, *http.Request)
|
||||
ListNamespaces(http.ResponseWriter, *http.Request)
|
||||
ListClusters(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
@@ -21,5 +20,4 @@ type Module interface {
|
||||
ListPods(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostablePods) (*inframonitoringtypes.Pods, error)
|
||||
ListNodes(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNodes) (*inframonitoringtypes.Nodes, error)
|
||||
ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNamespaces) (*inframonitoringtypes.Namespaces, error)
|
||||
ListClusters(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableClusters) (*inframonitoringtypes.Clusters, error)
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ func (module *module) getOrGetSetIdentity(ctx context.Context, serviceAccountID
|
||||
}
|
||||
|
||||
func (module *module) setRole(ctx context.Context, orgID valuer.UUID, id valuer.UUID, role *authtypes.Role) error {
|
||||
serviceAccount, err := module.Get(ctx, orgID, id)
|
||||
serviceAccount, err := module.GetWithRoles(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -387,12 +387,24 @@ func (module *module) setRole(ctx context.Context, orgID valuer.UUID, id valuer.
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.authz.Grant(ctx, orgID, []string{role.Name}, authtypes.MustNewSubject(coretypes.NewResourceServiceAccount(), id.String(), orgID, nil))
|
||||
err = module.authz.ModifyGrant(ctx, orgID, serviceAccount.RoleNames(), []string{role.Name}, authtypes.MustNewSubject(coretypes.NewResourceServiceAccount(), id.String(), orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.CreateServiceAccountRole(ctx, serviceAccountRole)
|
||||
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
err = module.store.DeleteServiceAccountRoles(ctx, serviceAccount.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.CreateServiceAccountRole(ctx, serviceAccountRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -207,6 +207,21 @@ func (store *store) CreateServiceAccountRole(ctx context.Context, serviceAccount
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) DeleteServiceAccountRoles(ctx context.Context, serviceAccountID valuer.UUID) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(serviceaccounttypes.ServiceAccountRole)).
|
||||
Where("service_account_id = ?", serviceAccountID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) DeleteServiceAccountRole(ctx context.Context, serviceAccountID valuer.UUID, roleID valuer.UUID) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
|
||||
@@ -413,21 +413,18 @@ func (b *traceOperatorCTEBuilder) buildFinalQuery(ctx context.Context, selectFro
|
||||
}
|
||||
|
||||
func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFromCTE string) (*qbtypes.Statement, error) {
|
||||
keySelectors := b.getKeySelectors()
|
||||
for _, field := range b.operator.SelectFields {
|
||||
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: field.FieldContext,
|
||||
FieldDataType: field.FieldDataType,
|
||||
})
|
||||
}
|
||||
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
// Select core fields
|
||||
sb.Select(
|
||||
"timestamp",
|
||||
"trace_id",
|
||||
"span_id",
|
||||
"name",
|
||||
"duration_nano",
|
||||
"parent_span_id",
|
||||
)
|
||||
|
||||
coreFields := []string{"timestamp", "trace_id", "span_id", "name", "duration_nano", "parent_span_id"}
|
||||
selectedFields := map[string]bool{
|
||||
"timestamp": true,
|
||||
"trace_id": true,
|
||||
@@ -437,23 +434,23 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom
|
||||
"parent_span_id": true,
|
||||
}
|
||||
|
||||
// CH 25.12.5 distributed-analyzer regression (ClickHouse/ClickHouse#103508):
|
||||
// native columns from a Distributed-table-backed CTE get renamed col → col_0
|
||||
// in the shard block, making them invisible to the outer SELECT/ORDER BY.
|
||||
// Fix: rename every core field to a safe alias (_s_<col>) in the inner SELECT
|
||||
// so the analyzer never sees the original name as an ORDER BY target. The
|
||||
// outer SELECT re-exposes them under their original names.
|
||||
innerCoreExprs := make([]string, len(coreFields))
|
||||
outerCoreExprs := make([]string, len(coreFields))
|
||||
for i, f := range coreFields {
|
||||
innerCoreExprs[i] = fmt.Sprintf("%s AS _s_%s", f, f)
|
||||
outerCoreExprs[i] = fmt.Sprintf("_s_%s AS %s", f, f)
|
||||
// Get keys for selectFields
|
||||
keySelectors := b.getKeySelectors()
|
||||
for _, field := range b.operator.SelectFields {
|
||||
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: field.FieldContext,
|
||||
FieldDataType: field.FieldDataType,
|
||||
})
|
||||
}
|
||||
|
||||
innerSB := sqlbuilder.NewSelectBuilder()
|
||||
innerSB.Select(innerCoreExprs...)
|
||||
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var additionalSelectedFields []string
|
||||
// Add selectFields using ColumnExpressionFor since we now have all base table columns
|
||||
for _, field := range b.operator.SelectFields {
|
||||
if selectedFields[field.Name] {
|
||||
continue
|
||||
@@ -464,56 +461,41 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom
|
||||
slog.String("field", field.Name), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
innerSB.SelectMore(colExpr)
|
||||
sb.SelectMore(colExpr)
|
||||
selectedFields[field.Name] = true
|
||||
additionalSelectedFields = append(additionalSelectedFields, field.Name)
|
||||
}
|
||||
|
||||
sb.From(selectFromCTE)
|
||||
|
||||
// Add order by support using ColumnExpressionFor
|
||||
orderApplied := false
|
||||
for _, orderBy := range b.operator.Order {
|
||||
if selectedFields[orderBy.Key.Name] {
|
||||
continue
|
||||
}
|
||||
colExpr, err := b.stmtBuilder.fm.ColumnExpressionFor(ctx, b.start, b.end, &orderBy.Key.TelemetryFieldKey, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
innerSB.SelectMore(colExpr)
|
||||
selectedFields[orderBy.Key.Name] = true
|
||||
sb.OrderBy(fmt.Sprintf("%s %s", colExpr, orderBy.Direction.StringValue()))
|
||||
orderApplied = true
|
||||
}
|
||||
|
||||
innerSB.From(selectFromCTE)
|
||||
innerSQL, innerArgs := innerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// Outer SELECT: re-exposes core fields under their original names and acts as
|
||||
// a projection barrier so ORDER BY-only fields do not leak into the result.
|
||||
outerSB := sqlbuilder.NewSelectBuilder()
|
||||
outerSB.Select(outerCoreExprs...)
|
||||
for _, name := range additionalSelectedFields {
|
||||
outerSB.SelectMore(fmt.Sprintf("`%s`", name))
|
||||
}
|
||||
outerSB.From(fmt.Sprintf("(%s) AS t", innerSQL))
|
||||
|
||||
if len(b.operator.Order) > 0 {
|
||||
for _, orderBy := range b.operator.Order {
|
||||
outerSB.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
|
||||
}
|
||||
} else {
|
||||
outerSB.OrderBy("timestamp DESC")
|
||||
if !orderApplied {
|
||||
sb.OrderBy("timestamp DESC")
|
||||
}
|
||||
|
||||
if b.operator.Limit > 0 {
|
||||
outerSB.Limit(b.operator.Limit)
|
||||
sb.Limit(b.operator.Limit)
|
||||
} else {
|
||||
outerSB.Limit(100)
|
||||
}
|
||||
if b.operator.Offset > 0 {
|
||||
outerSB.Offset(b.operator.Offset)
|
||||
sb.Limit(100)
|
||||
}
|
||||
|
||||
outerSQL, outerArgs := outerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
if b.operator.Offset > 0 {
|
||||
sb.Offset(b.operator.Offset)
|
||||
}
|
||||
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return &qbtypes.Statement{
|
||||
Query: outerSQL,
|
||||
Args: append(innerArgs, outerArgs...),
|
||||
Query: sql,
|
||||
Args: args,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id, `service.name` FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name` FROM A_DIR_DESC_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name` FROM A_DIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -104,7 +104,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_INDIR_DESC_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -141,7 +141,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_AND_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 15},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -178,7 +178,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_OR_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 20},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -215,7 +215,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_not_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -380,72 +380,11 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_DIR_DESC_B_AND_C_DIR_DESC_D) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
// order-by field (http.request.method) is not present in SelectFields;
|
||||
// it must be included in the inner SELECT so the outer ORDER BY can
|
||||
// reference it by alias, but must NOT appear in the outer SELECT list.
|
||||
name: "order by field not in select fields",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
operator: qbtypes.QueryBuilderTraceOperator{
|
||||
Expression: "A => B",
|
||||
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
||||
{
|
||||
Name: "service.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
Order: []qbtypes.OrderBy{
|
||||
{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "http.request.method",
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
Limit: 10,
|
||||
},
|
||||
compositeQuery: &qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'frontend'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'backend'",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
// http.request.method is in the inner SELECT (so ORDER BY can reach it)
|
||||
// but is absent from the outer SELECT column list — only the ORDER BY clause references it.
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id, `service.name` FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, attributes_string['http.request.method'] AS `http.request.method` FROM A_DIR_DESC_B) AS t ORDER BY `http.request.method` desc LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
type Clusters struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []ClusterRecord `json:"records" required:"true"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
|
||||
}
|
||||
|
||||
type ClusterRecord struct {
|
||||
// TODO(nikhilmantri0902): once the underlying attr key is migrated to
|
||||
// k8s.cluster.uid (see clusterNameAttrKey TODO in implinframonitoring),
|
||||
// surface ClusterUID alongside (or replace) ClusterName.
|
||||
ClusterName string `json:"clusterName" required:"true"`
|
||||
ClusterCPU float64 `json:"clusterCPU" required:"true"`
|
||||
ClusterCPUAllocatable float64 `json:"clusterCPUAllocatable" required:"true"`
|
||||
ClusterMemory float64 `json:"clusterMemory" required:"true"`
|
||||
ClusterMemoryAllocatable float64 `json:"clusterMemoryAllocatable" required:"true"`
|
||||
NodeCountsByReadiness NodeCountsByReadiness `json:"nodeCountsByReadiness" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableClusters is the request body for the v2 clusters list API.
|
||||
type PostableClusters struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableClusters contains acceptable values.
|
||||
func (req *PostableClusters) Validate() error {
|
||||
if req == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
|
||||
if req.Start <= 0 {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid start time %d: start must be greater than 0",
|
||||
req.Start,
|
||||
)
|
||||
}
|
||||
|
||||
if req.End <= 0 {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid end time %d: end must be greater than 0",
|
||||
req.End,
|
||||
)
|
||||
}
|
||||
|
||||
if req.Start >= req.End {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid time range: start (%d) must be less than end (%d)",
|
||||
req.Start,
|
||||
req.End,
|
||||
)
|
||||
}
|
||||
|
||||
if req.Limit < 1 || req.Limit > 5000 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "limit must be between 1 and 5000")
|
||||
}
|
||||
|
||||
if req.Offset < 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(ClustersValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
}
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON validates input immediately after decoding.
|
||||
func (req *PostableClusters) UnmarshalJSON(data []byte) error {
|
||||
type raw PostableClusters
|
||||
var decoded raw
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
*req = PostableClusters(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const (
|
||||
ClustersOrderByCPU = "cpu"
|
||||
ClustersOrderByCPUAllocatable = "cpu_allocatable"
|
||||
ClustersOrderByMemory = "memory"
|
||||
ClustersOrderByMemoryAllocatable = "memory_allocatable"
|
||||
)
|
||||
|
||||
var ClustersValidOrderByKeys = []string{
|
||||
ClustersOrderByCPU,
|
||||
ClustersOrderByCPUAllocatable,
|
||||
ClustersOrderByMemory,
|
||||
ClustersOrderByMemoryAllocatable,
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPostableClusters_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *PostableClusters
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid request",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil request",
|
||||
req: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time zero",
|
||||
req: &PostableClusters{
|
||||
Start: 0,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time negative",
|
||||
req: &PostableClusters{
|
||||
Start: -1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "end time zero",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 0,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time greater than end time",
|
||||
req: &PostableClusters{
|
||||
Start: 2000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time equal to end time",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit zero",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 0,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit negative",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: -10,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit exceeds max",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 5001,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "offset negative",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: -5,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy nil is valid",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key cpu and direction asc",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: ClustersOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key cpu_allocatable and direction desc",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: ClustersOrderByCPUAllocatable,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key memory and direction desc",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: ClustersOrderByMemory,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key memory_allocatable and direction asc",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: ClustersOrderByMemoryAllocatable,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with condition key is rejected",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "condition",
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy with pod_phase key is rejected",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "pod_phase",
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy with invalid key",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "unknown",
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key but invalid direction",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: ClustersOrderByMemory,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirection{String: valuer.NewString("invalid")},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.req.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Ast(err, errors.TypeInvalidInput), "expected error to be of type InvalidInput")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -245,6 +245,7 @@ type Store interface {
|
||||
|
||||
// Service Account Role
|
||||
CreateServiceAccountRole(context.Context, *ServiceAccountRole) error
|
||||
DeleteServiceAccountRoles(context.Context, valuer.UUID) error
|
||||
DeleteServiceAccountRole(context.Context, valuer.UUID, valuer.UUID) error
|
||||
|
||||
// Service Account Factor API Key
|
||||
|
||||
3
tests/fixtures/querier.py
vendored
3
tests/fixtures/querier.py
vendored
@@ -72,7 +72,6 @@ class TraceOperatorQuery:
|
||||
return_spans_from: str
|
||||
limit: int | None = None
|
||||
order: list[OrderBy] | None = None
|
||||
select_fields: list[TelemetryFieldKey] | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
spec: dict[str, Any] = {
|
||||
@@ -84,8 +83,6 @@ class TraceOperatorQuery:
|
||||
spec["limit"] = self.limit
|
||||
if self.order:
|
||||
spec["order"] = [o.to_dict() if hasattr(o, "to_dict") else o for o in self.order]
|
||||
if self.select_fields:
|
||||
spec["selectFields"] = [f.to_dict() for f in self.select_fields]
|
||||
return {"type": "builder_trace_operator", "spec": spec}
|
||||
|
||||
|
||||
|
||||
24
tests/fixtures/serviceaccount.py
vendored
24
tests/fixtures/serviceaccount.py
vendored
@@ -73,30 +73,6 @@ def get_first_key_id(signoz: types.SigNoz, token: str, service_account_id: str)
|
||||
return resp.json()["data"][0]["id"]
|
||||
|
||||
|
||||
def create_service_account_with_roles(signoz: types.SigNoz, token: str, name: str, roles: list[str]) -> str:
|
||||
"""Create a service account and assign multiple roles."""
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE),
|
||||
json={"name": name},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.CREATED, resp.text
|
||||
service_account_id = resp.json()["data"]["id"]
|
||||
|
||||
for role in roles:
|
||||
role_id = find_role_by_name(signoz, token, role)
|
||||
role_resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
json={"id": role_id},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert role_resp.status_code == HTTPStatus.NO_CONTENT, role_resp.text
|
||||
|
||||
return service_account_id
|
||||
|
||||
|
||||
def find_service_account_by_name(signoz: types.SigNoz, token: str, name: str) -> dict:
|
||||
"""Find a service account by name from the list endpoint."""
|
||||
list_resp = requests.get(
|
||||
|
||||
@@ -1,537 +0,0 @@
|
||||
"""
|
||||
Integration tests for TraceOperatorQuery (builder_trace_operator) through the
|
||||
/api/v5/query_range endpoint.
|
||||
|
||||
Covers:
|
||||
1. Basic trace operator (A => B) — returns matched spans from the correct trace.
|
||||
2. Order by a field absent from selectFields — must not return a server error.
|
||||
Guards against the ClickHouse NOT_FOUND_COLUMN_IN_BLOCK regression where
|
||||
ordering by a column absent from an outer SELECT caused a query failure.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import (
|
||||
OrderBy,
|
||||
TelemetryFieldKey,
|
||||
TraceOperatorQuery,
|
||||
make_query_request,
|
||||
)
|
||||
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
|
||||
|
||||
|
||||
def _builder_query(name: str, filter_expr: str, limit: int = 100) -> dict:
|
||||
return {
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": name,
|
||||
"signal": "traces",
|
||||
"filter": {"expression": filter_expr},
|
||||
"limit": limit,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_trace_operator_query_basic(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert one parent span and one child span in the same trace.
|
||||
|
||||
Tests:
|
||||
A => B (parent has a direct child) returns the parent span (returnSpansFrom=A)
|
||||
from the correct trace.
|
||||
"""
|
||||
parent_trace_id = TraceIdGenerator.trace_id()
|
||||
parent_span_id = TraceIdGenerator.span_id()
|
||||
child_span_id = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=parent_trace_id,
|
||||
span_id=parent_span_id,
|
||||
parent_span_id="",
|
||||
name="parent-op",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "parent"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=2),
|
||||
trace_id=parent_trace_id,
|
||||
span_id=child_span_id,
|
||||
parent_span_id=parent_span_id,
|
||||
name="child-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'parent'"),
|
||||
_builder_query("B", "operation.type = 'child'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["data"]["trace_id"] == parent_trace_id
|
||||
assert rows[0]["data"]["name"] == "parent-op"
|
||||
|
||||
|
||||
def test_trace_operator_query_order_by_field_not_in_select_fields(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Two traces, each with a grandparent → middle → grandchild chain:
|
||||
Trace 1: grandparent (svc-a, http.method=POST) → middle → grandchild
|
||||
Trace 2: grandparent (svc-b, http.method=GET) → middle → grandchild
|
||||
|
||||
Tests:
|
||||
A -> B (indirect descendant) with selectFields=[service.name] and
|
||||
order=[http.method DESC], where http.method is NOT in selectFields.
|
||||
|
||||
1. Query succeeds (no NOT_FOUND_COLUMN_IN_BLOCK error from ClickHouse).
|
||||
2. Results are actually ordered: POST sorts before GET descending, so
|
||||
svc-a must come before svc-b.
|
||||
"""
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
|
||||
gp_span_id_1 = TraceIdGenerator.span_id()
|
||||
mid_span_id_1 = TraceIdGenerator.span_id()
|
||||
gc_span_id_1 = TraceIdGenerator.span_id()
|
||||
|
||||
gp_span_id_2 = TraceIdGenerator.span_id()
|
||||
mid_span_id_2 = TraceIdGenerator.span_id()
|
||||
gc_span_id_2 = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
# Trace 1 — grandparent has http.method=POST (sorts first in DESC)
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=trace_id_1,
|
||||
span_id=gp_span_id_1,
|
||||
parent_span_id="",
|
||||
name="gp-op",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "grandparent", "http.method": "POST"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_1,
|
||||
span_id=mid_span_id_1,
|
||||
parent_span_id=gp_span_id_1,
|
||||
name="mid-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "middle"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=8),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_1,
|
||||
span_id=gc_span_id_1,
|
||||
parent_span_id=mid_span_id_1,
|
||||
name="gc-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "grandchild"},
|
||||
),
|
||||
# Trace 2 — grandparent has http.method=GET (sorts second in DESC)
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=trace_id_2,
|
||||
span_id=gp_span_id_2,
|
||||
parent_span_id="",
|
||||
name="gp-op",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-b"},
|
||||
attributes={"operation.type": "grandparent", "http.method": "GET"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=6),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_2,
|
||||
span_id=mid_span_id_2,
|
||||
parent_span_id=gp_span_id_2,
|
||||
name="mid-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-b"},
|
||||
attributes={"operation.type": "middle"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=5),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=gc_span_id_2,
|
||||
parent_span_id=mid_span_id_2,
|
||||
name="gc-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-b"},
|
||||
attributes={"operation.type": "grandchild"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'grandparent'"),
|
||||
_builder_query("B", "operation.type = 'grandchild'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A -> B", # indirect descendant
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
select_fields=[
|
||||
TelemetryFieldKey(name="service.name", field_data_type="string", field_context="resource"),
|
||||
],
|
||||
order=[
|
||||
# http.method is intentionally absent from select_fields
|
||||
OrderBy(
|
||||
key=TelemetryFieldKey(name="http.method", field_data_type="string", field_context="attribute"),
|
||||
direction="desc",
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
|
||||
# Both grandparent spans must be returned
|
||||
assert len(rows) == 2
|
||||
|
||||
# Ordering: POST > GET in DESC — svc-a (POST) must come before svc-b (GET)
|
||||
assert rows[0]["data"]["service.name"] == "svc-a", f"Expected svc-a (POST) first in http.method DESC order, got {rows[0]['data']['service.name']}"
|
||||
assert rows[1]["data"]["service.name"] == "svc-b", f"Expected svc-b (GET) second in http.method DESC order, got {rows[1]['data']['service.name']}"
|
||||
|
||||
|
||||
def test_trace_operator_query_order_by_select_field(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Two traces each with a parent → child pair; the parents have distinct durations
|
||||
(5 s and 1 s).
|
||||
|
||||
Tests:
|
||||
A => B with order=[duration_nano DESC] and no explicit selectFields.
|
||||
1. Query succeeds (no NOT_FOUND_COLUMN_IN_BLOCK error).
|
||||
2. Longer-duration parent (5 s) appears before the shorter one (1 s).
|
||||
"""
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
parent_span_id_1 = TraceIdGenerator.span_id()
|
||||
child_span_id_1 = TraceIdGenerator.span_id()
|
||||
parent_span_id_2 = TraceIdGenerator.span_id()
|
||||
child_span_id_2 = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=trace_id_1,
|
||||
span_id=parent_span_id_1,
|
||||
parent_span_id="",
|
||||
name="parent-long",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-long"},
|
||||
attributes={"operation.type": "parent"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_1,
|
||||
span_id=child_span_id_1,
|
||||
parent_span_id=parent_span_id_1,
|
||||
name="child-long",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-long"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=8),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=parent_span_id_2,
|
||||
parent_span_id="",
|
||||
name="parent-short",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-short"},
|
||||
attributes={"operation.type": "parent"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=child_span_id_2,
|
||||
parent_span_id=parent_span_id_2,
|
||||
name="child-short",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-short"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'parent'"),
|
||||
_builder_query("B", "operation.type = 'child'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
order=[
|
||||
OrderBy(
|
||||
key=TelemetryFieldKey(name="duration_nano", field_context="span"),
|
||||
direction="desc",
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
|
||||
assert len(rows) == 2
|
||||
# DESC: 5 s parent first, 1 s parent second
|
||||
assert rows[0]["data"]["name"] == "parent-long", f"Expected parent-long (5s) first in duration_nano DESC, got {rows[0]['data']['name']}"
|
||||
assert rows[1]["data"]["name"] == "parent-short", f"Expected parent-short (1s) second in duration_nano DESC, got {rows[1]['data']['name']}"
|
||||
|
||||
|
||||
def test_trace_operator_query_order_by_non_core_field_in_select(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Two traces each with a parent → child pair; parents have distinct http.method
|
||||
values (POST and GET).
|
||||
|
||||
Tests:
|
||||
A => B with selectFields=[http.method] and order=[http.method DESC].
|
||||
1. Query succeeds (no NOT_FOUND_COLUMN_IN_BLOCK error).
|
||||
2. http.method is present in every result row (it is in selectFields).
|
||||
3. Results are ordered DESC — POST before GET.
|
||||
"""
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
parent_span_id_1 = TraceIdGenerator.span_id()
|
||||
child_span_id_1 = TraceIdGenerator.span_id()
|
||||
parent_span_id_2 = TraceIdGenerator.span_id()
|
||||
child_span_id_2 = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_1,
|
||||
span_id=parent_span_id_1,
|
||||
parent_span_id="",
|
||||
name="parent-post",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-post"},
|
||||
attributes={"operation.type": "parent", "http.method": "POST"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_1,
|
||||
span_id=child_span_id_1,
|
||||
parent_span_id=parent_span_id_1,
|
||||
name="child-post",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-post"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=8),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_2,
|
||||
span_id=parent_span_id_2,
|
||||
parent_span_id="",
|
||||
name="parent-get",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-get"},
|
||||
attributes={"operation.type": "parent", "http.method": "GET"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=child_span_id_2,
|
||||
parent_span_id=parent_span_id_2,
|
||||
name="child-get",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-get"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
http_method_field = TelemetryFieldKey(
|
||||
name="http.method",
|
||||
field_data_type="string",
|
||||
field_context="attribute",
|
||||
)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'parent'"),
|
||||
_builder_query("B", "operation.type = 'child'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
select_fields=[http_method_field],
|
||||
order=[OrderBy(key=http_method_field, direction="desc")],
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
|
||||
assert len(rows) == 2
|
||||
# http.method must be present in every row (it is in selectFields)
|
||||
for row in rows:
|
||||
assert "http.method" in row["data"], f"http.method missing from row: {row['data']}"
|
||||
# DESC: POST before GET
|
||||
assert rows[0]["data"]["http.method"] == "POST", f"Expected POST first in http.method DESC, got {rows[0]['data']['http.method']}"
|
||||
assert rows[1]["data"]["http.method"] == "GET", f"Expected GET second in http.method DESC, got {rows[1]['data']['http.method']}"
|
||||
@@ -44,13 +44,13 @@ def test_assign_role_to_service_account(
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""POST /{id}/roles adds a role alongside existing ones."""
|
||||
"""POST /{id}/roles replaces existing role, verify via GET."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# create service account with viewer role
|
||||
service_account_id = create_service_account(signoz, token, "sa-assign-role", role="signoz-viewer")
|
||||
|
||||
# assign editor role (additive — viewer stays)
|
||||
# assign editor role (replaces viewer)
|
||||
editor_role_id = find_role_by_name(signoz, token, "signoz-editor")
|
||||
assign_resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
@@ -60,7 +60,7 @@ def test_assign_role_to_service_account(
|
||||
)
|
||||
assert assign_resp.status_code == HTTPStatus.NO_CONTENT, assign_resp.text
|
||||
|
||||
# verify both viewer and editor roles are present
|
||||
# verify only editor role is present (viewer was replaced)
|
||||
roles_resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -68,31 +68,9 @@ def test_assign_role_to_service_account(
|
||||
)
|
||||
assert roles_resp.status_code == HTTPStatus.OK, roles_resp.text
|
||||
role_names = [r["name"] for r in roles_resp.json()["data"]]
|
||||
assert len(role_names) == 2
|
||||
assert "signoz-viewer" in role_names
|
||||
assert len(role_names) == 1
|
||||
assert "signoz-editor" in role_names
|
||||
|
||||
# assign admin role — all three should be present
|
||||
admin_role_id = find_role_by_name(signoz, token, "signoz-admin")
|
||||
assign_resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
json={"id": admin_role_id},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert assign_resp.status_code == HTTPStatus.NO_CONTENT, assign_resp.text
|
||||
|
||||
roles_resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert roles_resp.status_code == HTTPStatus.OK, roles_resp.text
|
||||
role_names = [r["name"] for r in roles_resp.json()["data"]]
|
||||
assert len(role_names) == 3
|
||||
assert "signoz-viewer" in role_names
|
||||
assert "signoz-editor" in role_names
|
||||
assert "signoz-admin" in role_names
|
||||
assert "signoz-viewer" not in role_names
|
||||
|
||||
|
||||
def test_assign_role_idempotent(
|
||||
@@ -125,16 +103,16 @@ def test_assign_role_idempotent(
|
||||
assert role_names.count("signoz-viewer") == 1
|
||||
|
||||
|
||||
def test_assign_role_expands_access(
|
||||
def test_assign_role_replaces_access(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Adding a higher-privilege role expands the SA's access."""
|
||||
"""After role replacement, SA loses old permissions and gains new ones."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# create SA with viewer role and an API key
|
||||
service_account_id, api_key = create_service_account_with_key(signoz, token, "sa-role-expand-access", role="signoz-viewer")
|
||||
service_account_id, api_key = create_service_account_with_key(signoz, token, "sa-role-replace-access", role="signoz-viewer")
|
||||
|
||||
# viewer should get 403 on admin-only endpoint
|
||||
resp = requests.get(
|
||||
@@ -144,7 +122,7 @@ def test_assign_role_expands_access(
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.FORBIDDEN, f"Expected 403 for viewer on admin endpoint, got {resp.status_code}: {resp.text}"
|
||||
|
||||
# assign admin role (additive — viewer stays)
|
||||
# assign admin role (replaces viewer)
|
||||
admin_role_id = find_role_by_name(signoz, token, "signoz-admin")
|
||||
assign_resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
@@ -160,9 +138,9 @@ def test_assign_role_expands_access(
|
||||
headers={"SIGNOZ-API-KEY": api_key},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, f"Expected 200 after adding admin role, got {resp.status_code}: {resp.text}"
|
||||
assert resp.status_code == HTTPStatus.OK, f"Expected 200 for admin on admin endpoint, got {resp.status_code}: {resp.text}"
|
||||
|
||||
# verify both roles are present
|
||||
# verify only admin role is present
|
||||
roles_resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -170,9 +148,9 @@ def test_assign_role_expands_access(
|
||||
)
|
||||
assert roles_resp.status_code == HTTPStatus.OK, roles_resp.text
|
||||
role_names = [r["name"] for r in roles_resp.json()["data"]]
|
||||
assert len(role_names) == 2
|
||||
assert len(role_names) == 1
|
||||
assert "signoz-admin" in role_names
|
||||
assert "signoz-viewer" in role_names
|
||||
assert "signoz-viewer" not in role_names
|
||||
|
||||
|
||||
def test_remove_role_from_service_account(
|
||||
@@ -180,22 +158,13 @@ def test_remove_role_from_service_account(
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""DELETE /{id}/roles/{rid} revokes one role while keeping others."""
|
||||
"""DELETE /{id}/roles/{rid} revokes a role."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
service_account_id = create_service_account(signoz, token, "sa-remove-role", role="signoz-editor")
|
||||
|
||||
# add admin role (now has editor + admin)
|
||||
admin_role_id = find_role_by_name(signoz, token, "signoz-admin")
|
||||
assign_resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
json={"id": admin_role_id},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert assign_resp.status_code == HTTPStatus.NO_CONTENT, assign_resp.text
|
||||
|
||||
# remove editor role
|
||||
editor_role_id = find_role_by_name(signoz, token, "signoz-editor")
|
||||
|
||||
# remove the role
|
||||
resp = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles/{editor_role_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -203,7 +172,7 @@ def test_remove_role_from_service_account(
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.NO_CONTENT, resp.text
|
||||
|
||||
# verify editor is gone but admin remains
|
||||
# verify role is gone
|
||||
roles_resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -212,7 +181,6 @@ def test_remove_role_from_service_account(
|
||||
assert roles_resp.status_code == HTTPStatus.OK, roles_resp.text
|
||||
role_names = [r["name"] for r in roles_resp.json()["data"]]
|
||||
assert "signoz-editor" not in role_names
|
||||
assert "signoz-admin" in role_names
|
||||
|
||||
|
||||
def test_remove_role_verify_access_lost(
|
||||
|
||||
Reference in New Issue
Block a user