mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-15 05:00:28 +01:00
Compare commits
18 Commits
platform-p
...
traceop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6ef56cf22 | ||
|
|
357444c94e | ||
|
|
a8598f3bfa | ||
|
|
3787715fd8 | ||
|
|
0fbfff353a | ||
|
|
ca8cc30c34 | ||
|
|
85a8611f2e | ||
|
|
bca71f9a33 | ||
|
|
c93660357d | ||
|
|
5651e3b7a8 | ||
|
|
cf2cfbc7d4 | ||
|
|
f0b533b198 | ||
|
|
a969c38224 | ||
|
|
b892a0f0a5 | ||
|
|
4d47762eba | ||
|
|
77396a0bb3 | ||
|
|
28c05e1bab | ||
|
|
2b9e383994 |
@@ -2520,6 +2520,65 @@ 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:
|
||||
@@ -2599,6 +2658,54 @@ components:
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
namespaceCPU:
|
||||
format: double
|
||||
type: number
|
||||
namespaceMemory:
|
||||
format: double
|
||||
type: number
|
||||
namespaceName:
|
||||
type: string
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
required:
|
||||
- namespaceName
|
||||
- namespaceCPU
|
||||
- namespaceMemory
|
||||
- podCountsByPhase
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesNamespaces:
|
||||
properties:
|
||||
endTimeBeforeRetention:
|
||||
type: boolean
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
|
||||
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
|
||||
InframonitoringtypesNodeCondition:
|
||||
enum:
|
||||
- ready
|
||||
@@ -2776,6 +2883,32 @@ 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:
|
||||
@@ -2802,6 +2935,32 @@ components:
|
||||
- end
|
||||
- limit
|
||||
type: object
|
||||
InframonitoringtypesPostableNamespaces:
|
||||
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
|
||||
InframonitoringtypesPostableNodes:
|
||||
properties:
|
||||
end:
|
||||
@@ -11672,6 +11831,77 @@ 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
|
||||
@@ -11740,6 +11970,74 @@ paths:
|
||||
summary: List Hosts for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/namespaces:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'Returns a paginated list of Kubernetes namespaces with key aggregated
|
||||
pod metrics: CPU usage and memory working set (summed across pods in the group),
|
||||
plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown
|
||||
} from each pod''s latest k8s.pod.phase value in the window). Each namespace
|
||||
includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response
|
||||
type is ''list'' for the default k8s.namespace.name grouping or ''grouped_list''
|
||||
for custom groupBy keys; in both modes every row aggregates pods in the group.
|
||||
Supports filtering via a filter expression, custom groupBy, ordering by cpu
|
||||
/ memory, 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 (namespaceCPU, namespaceMemory) return -1 as a sentinel
|
||||
when no data is available for that field.'
|
||||
operationId: ListNamespaces
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPostableNamespaces'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaces'
|
||||
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 Namespaces for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/nodes:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -12,10 +12,14 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
InframonitoringtypesPostableClustersDTO,
|
||||
InframonitoringtypesPostableHostsDTO,
|
||||
InframonitoringtypesPostableNamespacesDTO,
|
||||
InframonitoringtypesPostableNodesDTO,
|
||||
InframonitoringtypesPostablePodsDTO,
|
||||
ListClusters200,
|
||||
ListHosts200,
|
||||
ListNamespaces200,
|
||||
ListNodes200,
|
||||
ListPods200,
|
||||
RenderErrorResponseDTO,
|
||||
@@ -24,6 +28,90 @@ 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
|
||||
@@ -108,6 +196,90 @@ export const useListHosts = <
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, 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 (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
|
||||
* @summary List Namespaces for Infra Monitoring
|
||||
*/
|
||||
export const listNamespaces = (
|
||||
inframonitoringtypesPostableNamespacesDTO: BodyType<InframonitoringtypesPostableNamespacesDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListNamespaces200>({
|
||||
url: `/api/v2/infra_monitoring/namespaces`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: inframonitoringtypesPostableNamespacesDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListNamespacesMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listNamespaces>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listNamespaces>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['listNamespaces'];
|
||||
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 listNamespaces>>,
|
||||
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return listNamespaces(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ListNamespacesMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listNamespaces>>
|
||||
>;
|
||||
export type ListNamespacesMutationBody =
|
||||
BodyType<InframonitoringtypesPostableNamespacesDTO>;
|
||||
export type ListNamespacesMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List Namespaces for Infra Monitoring
|
||||
*/
|
||||
export const useListNamespaces = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listNamespaces>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof listNamespaces>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getListNamespacesMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). 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 (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
|
||||
* @summary List Nodes for Infra Monitoring
|
||||
|
||||
@@ -4568,6 +4568,66 @@ 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
|
||||
@@ -4653,6 +4713,55 @@ export interface InframonitoringtypesHostsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesNamespaceRecordDTOMeta = {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
|
||||
export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
meta: InframonitoringtypesNamespaceRecordDTOMeta;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
namespaceCPU: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
namespaceMemory: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
namespaceName: string;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNamespacesDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
records: InframonitoringtypesNamespaceRecordDTO[] | null;
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total: number;
|
||||
type: InframonitoringtypesResponseTypeDTO;
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesNodeConditionDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
@@ -4836,6 +4945,34 @@ 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
|
||||
@@ -4864,6 +5001,34 @@ export interface InframonitoringtypesPostableHostsDTO {
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableNamespacesDTO {
|
||||
/**
|
||||
* @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 InframonitoringtypesPostableNodesDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -9247,6 +9412,14 @@ export type Healthz503 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListClusters200 = {
|
||||
data: InframonitoringtypesClustersDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListHosts200 = {
|
||||
data: InframonitoringtypesHostsDTO;
|
||||
/**
|
||||
@@ -9255,6 +9428,14 @@ export type ListHosts200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListNamespaces200 = {
|
||||
data: InframonitoringtypesNamespacesDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListNodes200 = {
|
||||
data: InframonitoringtypesNodesDTO;
|
||||
/**
|
||||
|
||||
@@ -2,8 +2,9 @@ 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 { LoadingOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -516,7 +517,9 @@ export default function CeleryOverviewTable({
|
||||
bordered={false}
|
||||
loading={{
|
||||
spinning: isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
indicator: (
|
||||
<Spin indicator={<Loader size={14} className="animate-spin" />} />
|
||||
),
|
||||
}}
|
||||
locale={{
|
||||
emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CaretDownOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { CaretDownOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Modal, Select, Spin, Tooltip, Tree, TreeDataNode } from 'antd';
|
||||
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -222,7 +223,7 @@ function AttributeCheckList({
|
||||
>
|
||||
{loading ? (
|
||||
<div className="loader-container">
|
||||
<Spin indicator={<LoadingOutlined spin />} size="large" />
|
||||
<Spin indicator={<Loader className="animate-spin" />} size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="modal-content">
|
||||
|
||||
@@ -7,12 +7,9 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import {
|
||||
DownOutlined,
|
||||
LoadingOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { DownOutlined, 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';
|
||||
@@ -1700,7 +1697,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
{loading && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
<LoadingOutlined />
|
||||
<Loader className="animate-spin" />
|
||||
</div>
|
||||
<div className="navigation-text">Refreshing values...</div>
|
||||
</div>
|
||||
@@ -1708,7 +1705,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
{!loading && waitingMessage && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
<LoadingOutlined />
|
||||
<Loader className="animate-spin" />
|
||||
</div>
|
||||
<div className="navigation-text" title={waitingMessage}>
|
||||
{waitingMessage}
|
||||
|
||||
@@ -6,13 +6,9 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
CloseOutlined,
|
||||
DownOutlined,
|
||||
LoadingOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { CloseOutlined, DownOutlined, 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';
|
||||
@@ -581,7 +577,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
{loading && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
<LoadingOutlined />
|
||||
<Loader className="animate-spin" />
|
||||
</div>
|
||||
<div className="navigation-text">Refreshing values...</div>
|
||||
</div>
|
||||
@@ -589,7 +585,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
{!loading && waitingMessage && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
<LoadingOutlined />
|
||||
<Loader className="animate-spin" />
|
||||
</div>
|
||||
<div className="navigation-text" title={waitingMessage}>
|
||||
{waitingMessage}
|
||||
|
||||
@@ -16,8 +16,8 @@ interface OverviewTabProps {
|
||||
account: ServiceAccountRow;
|
||||
localName: string;
|
||||
onNameChange: (v: string) => void;
|
||||
localRole: string;
|
||||
onRoleChange: (v: string | undefined) => void;
|
||||
localRoles: string[];
|
||||
onRolesChange: (v: string[]) => void;
|
||||
isDisabled: boolean;
|
||||
availableRoles: AuthtypesRoleDTO[];
|
||||
rolesLoading?: boolean;
|
||||
@@ -31,8 +31,8 @@ function OverviewTab({
|
||||
account,
|
||||
localName,
|
||||
onNameChange,
|
||||
localRole,
|
||||
onRoleChange,
|
||||
localRoles,
|
||||
onRolesChange,
|
||||
isDisabled,
|
||||
availableRoles,
|
||||
rolesLoading,
|
||||
@@ -95,10 +95,15 @@ function OverviewTab({
|
||||
{isDisabled ? (
|
||||
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
|
||||
<div className="sa-drawer__disabled-roles">
|
||||
{localRole ? (
|
||||
<Badge color="vanilla">
|
||||
{availableRoles.find((r) => r.id === localRole)?.name ?? localRole}
|
||||
</Badge>
|
||||
{localRoles.length > 0 ? (
|
||||
localRoles.map((roleId) => {
|
||||
const role = availableRoles.find((r) => r.id === roleId);
|
||||
return (
|
||||
<Badge key={roleId} color="vanilla">
|
||||
{role?.name ?? roleId}
|
||||
</Badge>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className="sa-drawer__input-text">—</span>
|
||||
)}
|
||||
@@ -108,14 +113,15 @@ function OverviewTab({
|
||||
) : (
|
||||
<RolesSelect
|
||||
id="sa-roles"
|
||||
mode="multiple"
|
||||
roles={availableRoles}
|
||||
loading={rolesLoading}
|
||||
isError={rolesError}
|
||||
error={rolesErrorObj}
|
||||
onRefetch={onRefetchRoles}
|
||||
value={localRole}
|
||||
onChange={onRoleChange}
|
||||
placeholder="Select role"
|
||||
value={localRoles}
|
||||
onChange={onRolesChange}
|
||||
placeholder="Select roles"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,7 @@ 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,
|
||||
@@ -37,7 +35,7 @@ import {
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
import APIError from 'types/api/error';
|
||||
import { retryOn429, toAPIError } from 'utils/errorUtils';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import AddKeyModal from './AddKeyModal';
|
||||
import DeleteAccountModal from './DeleteAccountModal';
|
||||
@@ -92,7 +90,7 @@ function ServiceAccountDrawer({
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
const [localName, setLocalName] = useState('');
|
||||
const [localRole, setLocalRole] = useState('');
|
||||
const [localRoles, setLocalRoles] = useState<string[]>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
|
||||
|
||||
@@ -140,7 +138,7 @@ function ServiceAccountDrawer({
|
||||
if (!account?.id) {
|
||||
roleSessionRef.current = null;
|
||||
} else if (account.id !== roleSessionRef.current && !isRolesLoading) {
|
||||
setLocalRole(currentRoles[0]?.id ?? '');
|
||||
setLocalRoles(currentRoles.map((r) => r.id).filter(Boolean) as string[]);
|
||||
roleSessionRef.current = account.id;
|
||||
}
|
||||
}, [account?.id, currentRoles, isRolesLoading]);
|
||||
@@ -151,7 +149,13 @@ function ServiceAccountDrawer({
|
||||
const isDirty =
|
||||
account !== null &&
|
||||
(localName !== (account.name ?? '') ||
|
||||
localRole !== (currentRoles[0]?.id ?? ''));
|
||||
JSON.stringify([...localRoles].sort()) !==
|
||||
JSON.stringify(
|
||||
currentRoles
|
||||
.map((r) => r.id)
|
||||
.filter(Boolean)
|
||||
.sort(),
|
||||
));
|
||||
|
||||
const {
|
||||
roles: availableRoles,
|
||||
@@ -179,27 +183,6 @@ 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) {
|
||||
@@ -267,7 +250,7 @@ function ServiceAccountDrawer({
|
||||
|
||||
const retryRolesUpdate = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const failures = await executeRolesOperation(selectedAccountId ?? '');
|
||||
const failures = await applyDiff([...localRoles], availableRoles);
|
||||
if (failures.length === 0) {
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Roles update'));
|
||||
} else {
|
||||
@@ -283,7 +266,7 @@ function ServiceAccountDrawer({
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [selectedAccountId, executeRolesOperation, failuresToSaveErrors]);
|
||||
}, [localRoles, availableRoles, applyDiff, failuresToSaveErrors]);
|
||||
|
||||
const handleSave = useCallback(async (): Promise<void> => {
|
||||
if (!account || !isDirty) {
|
||||
@@ -302,7 +285,7 @@ function ServiceAccountDrawer({
|
||||
|
||||
const [nameResult, rolesResult] = await Promise.allSettled([
|
||||
namePromise,
|
||||
executeRolesOperation(account.id),
|
||||
applyDiff([...localRoles], availableRoles),
|
||||
]);
|
||||
|
||||
const errors: SaveError[] = [];
|
||||
@@ -343,8 +326,10 @@ function ServiceAccountDrawer({
|
||||
account,
|
||||
isDirty,
|
||||
localName,
|
||||
localRoles,
|
||||
availableRoles,
|
||||
updateMutateAsync,
|
||||
executeRolesOperation,
|
||||
applyDiff,
|
||||
refetchAccount,
|
||||
onSuccess,
|
||||
queryClient,
|
||||
@@ -443,9 +428,9 @@ function ServiceAccountDrawer({
|
||||
account={account}
|
||||
localName={localName}
|
||||
onNameChange={handleNameChange}
|
||||
localRole={localRole}
|
||||
onRoleChange={(role): void => {
|
||||
setLocalRole(role ?? '');
|
||||
localRoles={localRoles}
|
||||
onRolesChange={(roles): void => {
|
||||
setLocalRoles(roles);
|
||||
clearRoleErrors();
|
||||
}}
|
||||
isDisabled={isDeleted}
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('ServiceAccountDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('changing roles enables Save; clicking Save sends role add request without delete', async () => {
|
||||
it('adding a role fires POST for the new role and no DELETE for existing roles', async () => {
|
||||
const roleSpy = jest.fn();
|
||||
const deleteSpy = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
@@ -171,6 +171,7 @@ 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'));
|
||||
|
||||
@@ -188,6 +189,43 @@ 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 { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Spin, SpinProps } from 'antd';
|
||||
|
||||
import { SpinerStyle } from './styles';
|
||||
@@ -7,7 +7,14 @@ 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={<LoadingOutlined spin />} />
|
||||
<Spin
|
||||
spinning
|
||||
size={size}
|
||||
tip={tip}
|
||||
indicator={
|
||||
<Loader className="animate-spin" role="img" aria-label="loading" />
|
||||
}
|
||||
/>
|
||||
</SpinerStyle>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'react';
|
||||
import type { TableComponents } from 'react-virtuoso';
|
||||
import { TableVirtuoso, TableVirtuosoHandle } from 'react-virtuoso';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { DndContext, pointerWithin } from '@dnd-kit/core';
|
||||
import {
|
||||
horizontalListSortingStrategy,
|
||||
@@ -580,7 +580,10 @@ function TanStackTableInner<TData>(
|
||||
className={viewStyles.tanstackLoadingOverlay}
|
||||
data-testid="tanstack-infinite-loader"
|
||||
>
|
||||
<Spin indicator={<LoadingOutlined spin />} tip="Loading more..." />
|
||||
<Spin
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
tip="Loading more..."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showPagination && pagination && (
|
||||
|
||||
@@ -398,7 +398,7 @@ describe('useTableParams (selective URL mode — partial config object)', () =>
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastExpanded).toBeDefined();
|
||||
expect(JSON.parse(lastExpanded!)).toEqual(
|
||||
expect(JSON.parse(lastExpanded!)).toStrictEqual(
|
||||
expect.arrayContaining(['row-1', 'row-2']),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { QueryFunctionContext, useQueries, useQuery } from 'react-query';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Spin, Switch, Table, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { getQueryRangeV5 } from 'api/v5/queryRange/getQueryRange';
|
||||
@@ -202,7 +202,9 @@ function TopErrors({
|
||||
columns={topErrorsColumnsConfig}
|
||||
loading={{
|
||||
spinning: isLoading || isRefetching,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
indicator: (
|
||||
<Spin indicator={<Loader size={14} className="animate-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 { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Spin, Table } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import emptyStateUrl from 'assets/Icons/emptyState.svg';
|
||||
@@ -205,7 +205,9 @@ function DomainList(): JSX.Element {
|
||||
columns={columnsConfig}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
indicator: (
|
||||
<Spin indicator={<Loader size={14} className="animate-spin" />} />
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useInterval } from 'react-use';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Compass, ScrollText } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { Compass, Loader, ScrollText } from '@signozhq/icons';
|
||||
import { Modal, Spin } from 'antd';
|
||||
import setRetentionApi from 'api/settings/setRetention';
|
||||
import setRetentionApiV2 from 'api/settings/setRetentionV2';
|
||||
@@ -480,7 +479,11 @@ function GeneralSettings({
|
||||
saveButtonText:
|
||||
metricsTtlValuesPayload.status === 'pending' ? (
|
||||
<span>
|
||||
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>{' '}
|
||||
{t('retention_save_button.pending', { name: 'metrics' })}
|
||||
</span>
|
||||
) : (
|
||||
@@ -523,7 +526,11 @@ function GeneralSettings({
|
||||
saveButtonText:
|
||||
tracesTtlValuesPayload.status === 'pending' ? (
|
||||
<span>
|
||||
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>{' '}
|
||||
{t('retention_save_button.pending', { name: 'traces' })}
|
||||
</span>
|
||||
) : (
|
||||
@@ -565,7 +572,11 @@ function GeneralSettings({
|
||||
saveButtonText:
|
||||
logsTtlValuesPayload.status === 'pending' ? (
|
||||
<span>
|
||||
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>{' '}
|
||||
{t('retention_save_button.pending', { name: 'logs' })}
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -9,11 +9,8 @@ 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 {
|
||||
LoadingOutlined,
|
||||
SearchOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Button, Input, Spin } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
@@ -338,7 +335,7 @@ function FullView({
|
||||
)}
|
||||
<div className="time-container">
|
||||
{response.isFetching && (
|
||||
<Spin spinning indicator={<LoadingOutlined spin />} />
|
||||
<Spin spinning indicator={<Loader className="animate-spin" />} />
|
||||
)}
|
||||
<TimePreference
|
||||
selectedTime={selectedTime}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { LoaderCircle } from '@signozhq/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 ? (
|
||||
<LoadingOutlined />
|
||||
<LoaderCircle className="animate-spin" size={12} />
|
||||
) : (
|
||||
<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,7 +21,13 @@ export default function QueryStatus(
|
||||
|
||||
const content = useMemo((): React.ReactElement => {
|
||||
if (loading) {
|
||||
return <Spin spinning size="small" indicator={<LoadingOutlined spin />} />;
|
||||
return (
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<LoaderCircle className="animate-spin" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Button, Popover, Spin } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -32,7 +32,13 @@ function FieldItem({
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return <Spin spinning size="small" indicator={<LoadingOutlined spin />} />;
|
||||
return (
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isHovered) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/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={<LoadingOutlined size={14} spin />}
|
||||
indicator={<Loader size={14} className="animate-spin" />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
CheckCircleTwoTone,
|
||||
CloseCircleTwoTone,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons';
|
||||
import { LoaderCircle } from '@signozhq/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import MessagingQueueHealthCheck from 'components/MessagingQueueHealthCheck/MessagingQueueHealthCheck';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -341,7 +338,7 @@ export default function ConnectionStatus(): JSX.Element {
|
||||
<div className="label"> Status </div>
|
||||
|
||||
<div className="status">
|
||||
{isQueryServiceLoading && <LoadingOutlined />}
|
||||
{isQueryServiceLoading && <LoaderCircle className="animate-spin" />}
|
||||
{!isQueryServiceLoading &&
|
||||
isReceivingData &&
|
||||
(getStartedSource !== 'kafka' ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { LoaderCircle } from '@signozhq/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 ? (
|
||||
<LoadingOutlined />
|
||||
<LoaderCircle className="animate-spin" />
|
||||
) : (
|
||||
<Check size={12} />
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { LoaderCircle } from '@signozhq/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 ? (
|
||||
<LoadingOutlined />
|
||||
<LoaderCircle className="animate-spin" />
|
||||
) : (
|
||||
<Check size={12} />
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
CheckCircleTwoTone,
|
||||
CloseCircleTwoTone,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons';
|
||||
import { LoaderCircle } from '@signozhq/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -245,7 +242,7 @@ export default function LogsConnectionStatus(): JSX.Element {
|
||||
<div className="label"> Status </div>
|
||||
|
||||
<div className="status">
|
||||
{(loading || isFetching) && <LoadingOutlined />}
|
||||
{(loading || isFetching) && <LoaderCircle className="animate-spin" />}
|
||||
{!(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,7 +28,17 @@ export function getDeploymentStageIcon(value: string): JSX.Element {
|
||||
switch (value) {
|
||||
case 'in_progress':
|
||||
return (
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 15 }} spin />} />
|
||||
<Spin
|
||||
indicator={
|
||||
<Loader
|
||||
size="large"
|
||||
className="animate-spin"
|
||||
role="img"
|
||||
aria-label="loading"
|
||||
data-icon="loading"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'deployed':
|
||||
return <CheckCircleFilled />;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { LinkOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { LinkOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -230,7 +231,7 @@ const useBaseAggregateOptions = ({
|
||||
key={key}
|
||||
icon={
|
||||
isLoading ? (
|
||||
<LoadingOutlined spin />
|
||||
<Loader className="animate-spin" />
|
||||
) : (
|
||||
<span style={{ color: aggregateData?.seriesColor }}>{icon}</span>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/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={<LoadingOutlined spin />}
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
>
|
||||
{selectedFunnelId && funnelDetails?.payload && (
|
||||
<FunnelProvider
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Button, Spin, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AxiosError } from 'axios';
|
||||
@@ -188,7 +189,9 @@ function Filters({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isFetching && <Spin indicator={<LoadingOutlined spin />} size="small" />}
|
||||
{isFetching && (
|
||||
<Spin indicator={<Loader className="animate-spin" />} size="small" />
|
||||
)}
|
||||
{error && (
|
||||
<Tooltip title={(error as AxiosError)?.message || 'Something went wrong'}>
|
||||
<InfoCircleOutlined size={14} />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useQueryClient } from 'react-query';
|
||||
import {
|
||||
getGetServiceAccountRolesQueryKey,
|
||||
useCreateServiceAccountRole,
|
||||
useDeleteServiceAccountRole,
|
||||
useGetServiceAccountRoles,
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
@@ -44,6 +45,9 @@ export function useServiceAccountRoleManager(
|
||||
const { mutateAsync: createRole } = useCreateServiceAccountRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
const { mutateAsync: deleteRole } = useDeleteServiceAccountRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
|
||||
const invalidateRoles = useCallback(
|
||||
() =>
|
||||
@@ -68,14 +72,21 @@ 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(
|
||||
@@ -106,7 +117,7 @@ export function useServiceAccountRoleManager(
|
||||
|
||||
return failures;
|
||||
},
|
||||
[accountId, currentRoles, createRole, invalidateRoles],
|
||||
[accountId, currentRoles, createRole, deleteRole, invalidateRoles],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('computeVisualLayout', () => {
|
||||
it('should handle empty input', () => {
|
||||
const layout = computeVisualLayout([]);
|
||||
expect(layout.totalVisualRows).toBe(0);
|
||||
expect(layout.visualRows).toEqual([]);
|
||||
expect(layout.visualRows).toStrictEqual([]);
|
||||
});
|
||||
|
||||
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]).toEqual([root]);
|
||||
expect(layout.visualRows[0]).toStrictEqual([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())).toEqual([]);
|
||||
expect(getVisibleSpans([], new Set())).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('returns single root span with no children', () => {
|
||||
const spans = [makeSpan({ span_id: 'root', level: 0 })];
|
||||
expect(getVisibleSpans(spans, new Set())).toEqual(spans);
|
||||
expect(getVisibleSpans(spans, new Set())).toStrictEqual(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())).toEqual(spans);
|
||||
expect(getVisibleSpans(spans, new Set())).toStrictEqual(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)).toEqual(spans);
|
||||
expect(getVisibleSpans(spans, uncollapsed)).toStrictEqual(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)).toEqual(['root', 'A']);
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual(['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)).toEqual([
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual([
|
||||
'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)).toEqual(['root']);
|
||||
expect(collapsed.map((s) => s.span_id)).toStrictEqual(['root']);
|
||||
|
||||
// Uncollapsed
|
||||
const uncollapsed = getVisibleSpans(spans, new Set(['root']));
|
||||
expect(uncollapsed.map((s) => s.span_id)).toEqual(['root', 'a', 'b']);
|
||||
expect(uncollapsed.map((s) => s.span_id)).toStrictEqual(['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)).toEqual(['L0', 'L1', 'L2']);
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual(['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)).toEqual(['root1', 'root2']);
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual(['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).toEqual(spans);
|
||||
expect(result).toStrictEqual(spans);
|
||||
});
|
||||
|
||||
it('handles 10k spans within 50ms', () => {
|
||||
@@ -218,7 +218,13 @@ 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)).toEqual(['root', 'A', 'B', 'C', 'C1']);
|
||||
expect(result.map((s) => s.span_id)).toStrictEqual([
|
||||
'root',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'C1',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -234,7 +240,7 @@ describe('getAncestorSpanIds', () => {
|
||||
makeSpan({ span_id: 'child', level: 1, parent_span_id: 'root' }),
|
||||
];
|
||||
const ancestors = getAncestorSpanIds(spans, 'child');
|
||||
expect(ancestors).toEqual(new Set(['root']));
|
||||
expect(ancestors).toStrictEqual(new Set(['root']));
|
||||
});
|
||||
|
||||
it('returns all ancestors for deeply nested span', () => {
|
||||
@@ -255,7 +261,7 @@ describe('getAncestorSpanIds', () => {
|
||||
makeSpan({ span_id: 'L3', level: 3, parent_span_id: 'L2' }),
|
||||
];
|
||||
const ancestors = getAncestorSpanIds(spans, 'L3');
|
||||
expect(ancestors).toEqual(new Set(['L2', 'L1', 'L0']));
|
||||
expect(ancestors).toStrictEqual(new Set(['L2', 'L1', 'L0']));
|
||||
});
|
||||
|
||||
it('returns empty set for unknown span', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Empty, Spin } from 'antd';
|
||||
import {
|
||||
BarController,
|
||||
@@ -97,7 +97,7 @@ function FunnelGraph(): JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<Spin spinning={isFetching} indicator={<LoadingOutlined spin />}>
|
||||
<Spin spinning={isFetching} indicator={<Loader className="animate-spin" />}>
|
||||
<div className={cx('funnel-graph', `funnel-graph--${totalSteps}-columns`)}>
|
||||
<div className="funnel-graph__chart-container">
|
||||
<canvas ref={canvasRef} />
|
||||
|
||||
@@ -54,9 +54,23 @@ describe('globalTimeStore', () => {
|
||||
expect(result.current.lastRefreshTimestamp).toBe(0);
|
||||
});
|
||||
|
||||
it('should have lastComputedMinMax with default values', () => {
|
||||
it('should have lastComputedMinMax computed from initial selectedTime', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.lastComputedMinMax).toStrictEqual({
|
||||
// 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({
|
||||
minTime: 0,
|
||||
maxTime: 0,
|
||||
});
|
||||
@@ -724,8 +738,8 @@ describe('globalTimeStore', () => {
|
||||
const wrapper = createIsolatedWrapper();
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Initial state has 0 values
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBe(0);
|
||||
// Initial state now has computed values (no longer 0)
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBeGreaterThan(0);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
|
||||
@@ -134,17 +134,17 @@ describe('useComputedMinMaxSync', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should compute min/max on mount when store has zero values', () => {
|
||||
it('should have computed min/max on store creation (no longer needs mount sync)', () => {
|
||||
const contextStore = createGlobalTimeStore({ selectedTime: '15m' });
|
||||
|
||||
expect(contextStore.getState().lastComputedMinMax).toStrictEqual({
|
||||
minTime: 0,
|
||||
maxTime: 0,
|
||||
});
|
||||
// Store now computes min/max on creation, not on mount
|
||||
expect(contextStore.getState().lastComputedMinMax.minTime).toBeGreaterThan(0);
|
||||
expect(contextStore.getState().lastComputedMinMax.maxTime).toBeGreaterThan(0);
|
||||
|
||||
// Hook still works but is a no-op when values already exist
|
||||
renderHook(() => useComputedMinMaxSync(contextStore));
|
||||
|
||||
// Should have computed values now
|
||||
// Values remain computed
|
||||
expect(contextStore.getState().lastComputedMinMax.maxTime).toBeGreaterThan(0);
|
||||
expect(contextStore.getState().lastComputedMinMax.minTime).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
computeRounded5sMinMax,
|
||||
isCustomTimeRange,
|
||||
parseSelectedTime,
|
||||
safeParseSelectedTime,
|
||||
} from './utils';
|
||||
|
||||
export type GlobalTimeStoreApi = StoreApi<GlobalTimeStore>;
|
||||
@@ -40,7 +41,7 @@ export function createGlobalTimeStore(
|
||||
refreshInterval,
|
||||
isRefreshEnabled: computeIsRefreshEnabled(selectedTime, refreshInterval),
|
||||
lastRefreshTimestamp: 0,
|
||||
lastComputedMinMax: { minTime: 0, maxTime: 0 },
|
||||
lastComputedMinMax: safeParseSelectedTime(selectedTime),
|
||||
|
||||
setSelectedTime: (
|
||||
time: GlobalTimeSelectedTime,
|
||||
|
||||
@@ -61,6 +61,8 @@ 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)) {
|
||||
@@ -78,6 +80,18 @@ 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);
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -46,80 +44,54 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/channels", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateChannel),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateChannel",
|
||||
Tags: []string{"channels"},
|
||||
Summary: "Create notification channel",
|
||||
Description: "This endpoint creates a notification channel",
|
||||
Request: new(alertmanagertypes.PostableChannel),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(alertmanagertypes.Channel),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesNotificationChannel,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/channels", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateChannel), handler.OpenAPIDef{
|
||||
ID: "CreateChannel",
|
||||
Tags: []string{"channels"},
|
||||
Summary: "Create notification channel",
|
||||
Description: "This endpoint creates a notification channel",
|
||||
Request: new(alertmanagertypes.PostableChannel),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(alertmanagertypes.Channel),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/channels/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateChannelByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateChannelByID",
|
||||
Tags: []string{"channels"},
|
||||
Summary: "Update notification channel",
|
||||
Description: "This endpoint updates a notification channel by ID",
|
||||
Request: new(alertmanagertypes.Receiver),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceNotificationChannel,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateChannelByID), handler.OpenAPIDef{
|
||||
ID: "UpdateChannelByID",
|
||||
Tags: []string{"channels"},
|
||||
Summary: "Update notification channel",
|
||||
Description: "This endpoint updates a notification channel by ID",
|
||||
Request: new(alertmanagertypes.Receiver),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/channels/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteChannelByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteChannelByID",
|
||||
Tags: []string{"channels"},
|
||||
Summary: "Delete notification channel",
|
||||
Description: "This endpoint deletes a notification channel by ID",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceNotificationChannel,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteChannelByID), handler.OpenAPIDef{
|
||||
ID: "DeleteChannelByID",
|
||||
Tags: []string{"channels"},
|
||||
Summary: "Delete notification channel",
|
||||
Description: "This endpoint deletes a notification channel by ID",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -191,91 +163,54 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/route_policies", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateRoutePolicy),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateRoutePolicy",
|
||||
Tags: []string{"routepolicies"},
|
||||
Summary: "Create route policy",
|
||||
Description: "This endpoint creates a route policy",
|
||||
Request: new(alertmanagertypes.PostableRoutePolicy),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(alertmanagertypes.GettableRoutePolicy),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(
|
||||
handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesRoutePolicy,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.ResponseJSONPath("data.id"),
|
||||
},
|
||||
handler.AttachManyAuditDef{
|
||||
AttachedResource: coretypes.ResourceMetaResourceNotificationChannel,
|
||||
AttachedResourceIDs: handler.BodyJSONArray("channels"),
|
||||
TargetResource: coretypes.ResourceMetaResourceRoutePolicy,
|
||||
TargetResourceID: handler.ResponseJSONPath("data.id"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
},
|
||||
),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateRoutePolicy), handler.OpenAPIDef{
|
||||
ID: "CreateRoutePolicy",
|
||||
Tags: []string{"routepolicies"},
|
||||
Summary: "Create route policy",
|
||||
Description: "This endpoint creates a route policy",
|
||||
Request: new(alertmanagertypes.PostableRoutePolicy),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(alertmanagertypes.GettableRoutePolicy),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateRoutePolicy),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateRoutePolicy",
|
||||
Tags: []string{"routepolicies"},
|
||||
Summary: "Update route policy",
|
||||
Description: "This endpoint updates a route policy by ID",
|
||||
Request: new(alertmanagertypes.PostableRoutePolicy),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(alertmanagertypes.GettableRoutePolicy),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceRoutePolicy,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateRoutePolicy), handler.OpenAPIDef{
|
||||
ID: "UpdateRoutePolicy",
|
||||
Tags: []string{"routepolicies"},
|
||||
Summary: "Update route policy",
|
||||
Description: "This endpoint updates a route policy by ID",
|
||||
Request: new(alertmanagertypes.PostableRoutePolicy),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(alertmanagertypes.GettableRoutePolicy),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteRoutePolicyByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteRoutePolicyByID",
|
||||
Tags: []string{"routepolicies"},
|
||||
Summary: "Delete route policy",
|
||||
Description: "This endpoint deletes a route policy by ID",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceRoutePolicy,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteRoutePolicyByID), handler.OpenAPIDef{
|
||||
ID: "DeleteRoutePolicyByID",
|
||||
Tags: []string{"routepolicies"},
|
||||
Summary: "Delete route policy",
|
||||
Description: "This endpoint deletes a route policy by ID",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -29,28 +27,20 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/domains", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Create),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Create auth domain",
|
||||
Description: "This endpoint creates an auth domain",
|
||||
Request: new(authtypes.PostableAuthDomain),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesAuthDomain,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Create), handler.OpenAPIDef{
|
||||
ID: "CreateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Create auth domain",
|
||||
Description: "This endpoint creates an auth domain",
|
||||
Request: new(authtypes.PostableAuthDomain),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -71,55 +61,37 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/domains/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Update),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Update auth domain",
|
||||
Description: "This endpoint updates an auth domain",
|
||||
Request: new(authtypes.UpdatableAuthDomain),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Update), handler.OpenAPIDef{
|
||||
ID: "UpdateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Update auth domain",
|
||||
Description: "This endpoint updates an auth domain",
|
||||
Request: new(authtypes.UpdatableAuthDomain),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/domains/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Delete auth domain",
|
||||
Description: "This endpoint deletes an auth domain",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
|
||||
ID: "DeleteAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Delete auth domain",
|
||||
Description: "This endpoint deletes an auth domain",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -48,11 +46,6 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesCloudIntegration,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,12 +106,6 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceCloudIntegration,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -139,12 +126,6 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceCloudIntegration,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -207,12 +188,6 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceCloudIntegrationService,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("service_id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
@@ -15,29 +14,20 @@ import (
|
||||
)
|
||||
|
||||
func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreatePublicDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Create public dashboard",
|
||||
Description: "This endpoint creates public sharing config and enables public sharing of the dashboard",
|
||||
Request: new(dashboardtypes.PostablePublicDashboard),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesPublicDashboard,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{
|
||||
ID: "CreatePublicDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Create public dashboard",
|
||||
Description: "This endpoint creates public sharing config and enables public sharing of the dashboard",
|
||||
Request: new(dashboardtypes.PostablePublicDashboard),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -58,55 +48,37 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.dashboardHandler.UpdatePublic),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdatePublicDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Update public dashboard",
|
||||
Description: "This endpoint updates the public sharing config for a dashboard",
|
||||
Request: new(dashboardtypes.UpdatablePublicDashboard),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcePublicDashboard,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.UpdatePublic), handler.OpenAPIDef{
|
||||
ID: "UpdatePublicDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Update public dashboard",
|
||||
Description: "This endpoint updates the public sharing config for a dashboard",
|
||||
Request: new(dashboardtypes.UpdatablePublicDashboard),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.dashboardHandler.DeletePublic),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeletePublicDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Delete public dashboard",
|
||||
Description: "This endpoint deletes the public sharing config and disables the public sharing of a dashboard",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcePublicDashboard,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.DeletePublic), handler.OpenAPIDef{
|
||||
ID: "DeletePublicDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Delete public dashboard",
|
||||
Description: "This endpoint deletes the public sharing config and disables the public sharing of a dashboard",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/gatewaytypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -48,169 +46,105 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKey),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateIngestionKey",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Create ingestion key for workspace",
|
||||
Description: "This endpoint creates an ingestion key for the workspace",
|
||||
Request: new(gatewaytypes.PostableIngestionKey),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(gatewaytypes.GettableCreatedIngestionKey),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesIngestionKey,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.ResponseJSONPath("data.id"),
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKey), handler.OpenAPIDef{
|
||||
ID: "CreateIngestionKey",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Create ingestion key for workspace",
|
||||
Description: "This endpoint creates an ingestion key for the workspace",
|
||||
Request: new(gatewaytypes.PostableIngestionKey),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(gatewaytypes.GettableCreatedIngestionKey),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKey),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateIngestionKey",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Update ingestion key for workspace",
|
||||
Description: "This endpoint updates an ingestion key for the workspace",
|
||||
Request: new(gatewaytypes.PostableIngestionKey),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceIngestionKey,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("keyId"),
|
||||
}),
|
||||
)).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKey), handler.OpenAPIDef{
|
||||
ID: "UpdateIngestionKey",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Update ingestion key for workspace",
|
||||
Description: "This endpoint updates an ingestion key for the workspace",
|
||||
Request: new(gatewaytypes.PostableIngestionKey),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKey),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteIngestionKey",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Delete ingestion key for workspace",
|
||||
Description: "This endpoint deletes an ingestion key for the workspace",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceIngestionKey,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("keyId"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKey), handler.OpenAPIDef{
|
||||
ID: "DeleteIngestionKey",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Delete ingestion key for workspace",
|
||||
Description: "This endpoint deletes an ingestion key for the workspace",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKeyLimit),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateIngestionKeyLimit",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Create limit for the ingestion key",
|
||||
Description: "This endpoint creates an ingestion key limit",
|
||||
Request: new(gatewaytypes.PostableIngestionKeyLimit),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(gatewaytypes.GettableCreatedIngestionKeyLimit),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(
|
||||
handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesIngestionLimit,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.ResponseJSONPath("data.id"),
|
||||
},
|
||||
handler.AttachAuditDef{
|
||||
AttachedResource: coretypes.ResourceMetaResourceIngestionLimit,
|
||||
AttachedResourceID: handler.ResponseJSONPath("data.id"),
|
||||
TargetResource: coretypes.ResourceMetaResourceIngestionKey,
|
||||
TargetResourceID: handler.PathParam("keyId"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
},
|
||||
),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKeyLimit), handler.OpenAPIDef{
|
||||
ID: "CreateIngestionKeyLimit",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Create limit for the ingestion key",
|
||||
Description: "This endpoint creates an ingestion key limit",
|
||||
Request: new(gatewaytypes.PostableIngestionKeyLimit),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(gatewaytypes.GettableCreatedIngestionKeyLimit),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKeyLimit),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateIngestionKeyLimit",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Update limit for the ingestion key",
|
||||
Description: "This endpoint updates an ingestion key limit",
|
||||
Request: new(gatewaytypes.UpdatableIngestionKeyLimit),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceIngestionLimit,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("limitId"),
|
||||
}),
|
||||
)).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKeyLimit), handler.OpenAPIDef{
|
||||
ID: "UpdateIngestionKeyLimit",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Update limit for the ingestion key",
|
||||
Description: "This endpoint updates an ingestion key limit",
|
||||
Request: new(gatewaytypes.UpdatableIngestionKeyLimit),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKeyLimit),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteIngestionKeyLimit",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Delete limit for the ingestion key",
|
||||
Description: "This endpoint deletes an ingestion key limit",
|
||||
Request: nil,
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceIngestionLimit,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("limitId"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKeyLimit), handler.OpenAPIDef{
|
||||
ID: "DeleteIngestionKeyLimit",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Delete limit for the ingestion key",
|
||||
Description: "This endpoint deletes an ingestion key limit",
|
||||
Request: nil,
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -67,5 +67,43 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/infra_monitoring/namespaces", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListNamespaces),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListNamespaces",
|
||||
Tags: []string{"inframonitoring"},
|
||||
Summary: "List Namespaces for Infra Monitoring",
|
||||
Description: "Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, 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 (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.",
|
||||
Request: new(inframonitoringtypes.PostableNamespaces),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(inframonitoringtypes.Namespaces),
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -47,11 +45,6 @@ func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesLLMPricingRule,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -92,12 +85,6 @@ func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceLLMPricingRule,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricsexplorertypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -124,14 +122,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceMetricField,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("metric_name"),
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -28,28 +26,20 @@ func (provider *provider) addOrgRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/orgs/me", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.orgHandler.Update),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateMyOrganization",
|
||||
Tags: []string{"orgs"},
|
||||
Summary: "Update my organization",
|
||||
Description: "This endpoint updates the organization I belong to",
|
||||
Request: new(types.Organization),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusConflict, http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceOrganization,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.AdminAccess(provider.orgHandler.Update), handler.OpenAPIDef{
|
||||
ID: "UpdateMyOrganization",
|
||||
Tags: []string{"orgs"},
|
||||
Summary: "Update my organization",
|
||||
Description: "This endpoint updates the organization I belong to",
|
||||
Request: new(types.Organization),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusConflict, http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/preferencetypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -46,29 +44,20 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.preferenceHandler.UpdateByUser),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateUserPreference",
|
||||
Tags: []string{"preferences"},
|
||||
Summary: "Update user preference",
|
||||
Description: "This endpoint updates the user preference by name",
|
||||
Request: new(preferencetypes.UpdatablePreference),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceUserPreference,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("name"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(provider.authzMiddleware.ViewAccess(provider.preferenceHandler.UpdateByUser), handler.OpenAPIDef{
|
||||
ID: "UpdateUserPreference",
|
||||
Tags: []string{"preferences"},
|
||||
Summary: "Update user preference",
|
||||
Description: "This endpoint updates the user preference by name",
|
||||
Request: new(preferencetypes.UpdatablePreference),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -106,29 +95,20 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.preferenceHandler.UpdateByOrg),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateOrgPreference",
|
||||
Tags: []string{"preferences"},
|
||||
Summary: "Update org preference",
|
||||
Description: "This endpoint updates the org preference by name",
|
||||
Request: new(preferencetypes.UpdatablePreference),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceOrgPreference,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("name"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.UpdateByOrg), handler.OpenAPIDef{
|
||||
ID: "UpdateOrgPreference",
|
||||
Tags: []string{"preferences"},
|
||||
Summary: "Update org preference",
|
||||
Description: "This endpoint updates the org preference by name",
|
||||
Request: new(preferencetypes.UpdatablePreference),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,34 +5,24 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/promotetypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addPromoteRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.promoteHandler.HandlePromoteAndIndexPaths),
|
||||
handler.OpenAPIDef{
|
||||
ID: "HandlePromoteAndIndexPaths",
|
||||
Tags: []string{"logs"},
|
||||
Summary: "Promote and index paths",
|
||||
Description: "This endpoints promotes and indexes paths",
|
||||
Request: new([]*promotetypes.PromotePath),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesLogsField,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(provider.authzMiddleware.EditAccess(provider.promoteHandler.HandlePromoteAndIndexPaths), handler.OpenAPIDef{
|
||||
ID: "HandlePromoteAndIndexPaths",
|
||||
Tags: []string{"logs"},
|
||||
Summary: "Promote and index paths",
|
||||
Description: "This endpoints promotes and indexes paths",
|
||||
Request: new([]*promotetypes.PromotePath),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ func (handler *healthOpenAPIHandler) ServeOpenAPI(opCtx openapi.OperationContext
|
||||
)
|
||||
}
|
||||
|
||||
func (handler *healthOpenAPIHandler) AuditDefs() []pkghandler.AuditDef {
|
||||
func (handler *healthOpenAPIHandler) AuditDef() *pkghandler.AuditDef {
|
||||
// Health endpoints are not audited since they don't represent user actions and are called frequently by monitoring systems, which would create noise in the audit logs.
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,35 +5,26 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/roles", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.authzHandler.Create),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Create role",
|
||||
Description: "This endpoint creates a role",
|
||||
Request: new(authtypes.PostableRole),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesRole,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Create), handler.OpenAPIDef{
|
||||
ID: "CreateRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Create role",
|
||||
Description: "This endpoint creates a role",
|
||||
Request: new(authtypes.PostableRole),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -88,81 +79,54 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.authzHandler.Patch),
|
||||
handler.OpenAPIDef{
|
||||
ID: "PatchRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Patch role",
|
||||
Description: "This endpoint patches a role",
|
||||
Request: new(authtypes.PatchableRole),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceRole,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Patch), handler.OpenAPIDef{
|
||||
ID: "PatchRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Patch role",
|
||||
Description: "This endpoint patches a role",
|
||||
Request: new(authtypes.PatchableRole),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.authzHandler.PatchObjects),
|
||||
handler.OpenAPIDef{
|
||||
ID: "PatchObjects",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Patch objects for a role by relation",
|
||||
Description: "Patches the objects connected to the specified role via a given relation type",
|
||||
Request: new(coretypes.PatchableObjects),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceRole,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.PatchObjects), handler.OpenAPIDef{
|
||||
ID: "PatchObjects",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Patch objects for a role by relation",
|
||||
Description: "Patches the objects connected to the specified role via a given relation type",
|
||||
Request: new(coretypes.PatchableObjects),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.authzHandler.Delete),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Delete role",
|
||||
Description: "This endpoint deletes a role",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceRole,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Delete), handler.OpenAPIDef{
|
||||
ID: "DeleteRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Delete role",
|
||||
Description: "This endpoint deletes a role",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -39,110 +37,64 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateRule),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateRule",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Create alert rule",
|
||||
Description: "This endpoint creates a new alert rule",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
Response: new(ruletypes.Rule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(
|
||||
handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesRule,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.ResponseJSONPath("data.id"),
|
||||
},
|
||||
handler.AttachManyAuditDef{
|
||||
AttachedResource: coretypes.ResourceMetaResourceNotificationChannel,
|
||||
AttachedResourceIDs: handler.BodyJSONArray("preferredChannels"),
|
||||
TargetResource: coretypes.ResourceMetaResourceRule,
|
||||
TargetResourceID: handler.ResponseJSONPath("data.id"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
},
|
||||
),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/rules", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateRule), handler.OpenAPIDef{
|
||||
ID: "CreateRule",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Create alert rule",
|
||||
Description: "This endpoint creates a new alert rule",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
Response: new(ruletypes.Rule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateRuleByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Update alert rule",
|
||||
Description: "This endpoint updates an alert rule by ID",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceRule,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateRuleByID), handler.OpenAPIDef{
|
||||
ID: "UpdateRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Update alert rule",
|
||||
Description: "This endpoint updates an alert rule by ID",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteRuleByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Delete alert rule",
|
||||
Description: "This endpoint deletes an alert rule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceRule,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteRuleByID), handler.OpenAPIDef{
|
||||
ID: "DeleteRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Delete alert rule",
|
||||
Description: "This endpoint deletes an alert rule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.rulerHandler.PatchRuleByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "PatchRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Patch alert rule",
|
||||
Description: "This endpoint applies a partial update to an alert rule by ID",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
Response: new(ruletypes.Rule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceRule,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.PatchRuleByID), handler.OpenAPIDef{
|
||||
ID: "PatchRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Patch alert rule",
|
||||
Description: "This endpoint applies a partial update to an alert rule by ID",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
Response: new(ruletypes.Rule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -191,82 +143,45 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateDowntimeSchedule),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateDowntimeSchedule",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Create downtime schedule",
|
||||
Description: "This endpoint creates a new planned maintenance / downtime schedule",
|
||||
Request: new(ruletypes.PostablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.PlannedMaintenance),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(
|
||||
handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesPlannedMaintenance,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.ResponseJSONPath("data.id"),
|
||||
},
|
||||
handler.AttachManyAuditDef{
|
||||
AttachedResource: coretypes.ResourceMetaResourceRule,
|
||||
AttachedResourceIDs: handler.BodyJSONArray("alertIds"),
|
||||
TargetResource: coretypes.ResourceMetaResourcePlannedMaintenance,
|
||||
TargetResourceID: handler.ResponseJSONPath("data.id"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
},
|
||||
),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateDowntimeSchedule), handler.OpenAPIDef{
|
||||
ID: "CreateDowntimeSchedule",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Create downtime schedule",
|
||||
Description: "This endpoint creates a new planned maintenance / downtime schedule",
|
||||
Request: new(ruletypes.PostablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.PlannedMaintenance),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateDowntimeScheduleByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Update downtime schedule",
|
||||
Description: "This endpoint updates a downtime schedule by ID",
|
||||
Request: new(ruletypes.PostablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcePlannedMaintenance,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateDowntimeScheduleByID), handler.OpenAPIDef{
|
||||
ID: "UpdateDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Update downtime schedule",
|
||||
Description: "This endpoint updates a downtime schedule by ID",
|
||||
Request: new(ruletypes.PostablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteDowntimeScheduleByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Delete downtime schedule",
|
||||
Description: "This endpoint deletes a downtime schedule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcePlannedMaintenance,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteDowntimeScheduleByID), handler.OpenAPIDef{
|
||||
ID: "DeleteDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Delete downtime schedule",
|
||||
Description: "This endpoint deletes a downtime schedule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
|
||||
@@ -18,31 +17,22 @@ import (
|
||||
)
|
||||
|
||||
func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/service_accounts", handler.New(
|
||||
provider.authzMiddleware.Check(provider.serviceAccountHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceMetaResourcesServiceAccount, serviceAccountCollectionSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Create service account",
|
||||
Description: "This endpoint creates a service account",
|
||||
Request: new(serviceaccounttypes.PostableServiceAccount),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbCreate)}),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesServiceAccount,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.ResponseJSONPath("data.id"),
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceMetaResourcesServiceAccount, serviceAccountCollectionSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Create service account",
|
||||
Description: "This endpoint creates a service account",
|
||||
Request: new(serviceaccounttypes.PostableServiceAccount),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbCreate)}),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -120,192 +110,125 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(
|
||||
provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.SetRole, []middleware.AuthZCheckGroup{
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromBody, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
}),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccountRole",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Create service account role",
|
||||
Description: "This endpoint assigns a role to a service account",
|
||||
Request: new(serviceaccounttypes.PostableServiceAccountRole),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
|
||||
},
|
||||
handler.WithAuditDef(handler.AttachAuditDef{
|
||||
AttachedResource: coretypes.ResourceRole,
|
||||
AttachedResourceID: handler.BodyJSONPath("id"),
|
||||
TargetResource: coretypes.ResourceServiceAccount,
|
||||
TargetResourceID: handler.PathParam("id"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(
|
||||
provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.DeleteRole, []middleware.AuthZCheckGroup{
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromPath, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
}),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteServiceAccountRole",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Delete service account role",
|
||||
Description: "This endpoint revokes a role from service account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
|
||||
},
|
||||
handler.WithAuditDef(handler.AttachAuditDef{
|
||||
AttachedResource: coretypes.ResourceRole,
|
||||
AttachedResourceID: handler.PathParam("rid"),
|
||||
TargetResource: coretypes.ResourceServiceAccount,
|
||||
TargetResourceID: handler.PathParam("id"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/me", handler.New(
|
||||
provider.authzMiddleware.OpenAccess(provider.serviceAccountHandler.UpdateMe),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateMyServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Updates my service account",
|
||||
Description: "This endpoint gets my service account",
|
||||
Request: new(serviceaccounttypes.UpdatableServiceAccount),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: nil,
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceServiceAccount,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(
|
||||
provider.authzMiddleware.Check(provider.serviceAccountHandler.Update, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.SetRole, []middleware.AuthZCheckGroup{
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Updates a service account",
|
||||
Description: "This endpoint updates an existing service account",
|
||||
Request: new(serviceaccounttypes.UpdatableServiceAccount),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceServiceAccount,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
}}},
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromBody, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccountRole",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Create service account role",
|
||||
Description: "This endpoint assigns a role to a service account",
|
||||
Request: new(serviceaccounttypes.PostableServiceAccountRole),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(
|
||||
provider.authzMiddleware.Check(provider.serviceAccountHandler.Delete, authtypes.Relation{Verb: coretypes.VerbDelete}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.DeleteRole, []middleware.AuthZCheckGroup{
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Deletes a service account",
|
||||
Description: "This endpoint deletes an existing service account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbDelete)}),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceServiceAccount,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
}}},
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromPath, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "DeleteServiceAccountRole",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Delete service account role",
|
||||
Description: "This endpoint revokes a role from service account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(
|
||||
provider.authzMiddleware.Check(provider.serviceAccountHandler.CreateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccountKey",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Create a service account key",
|
||||
Description: "This endpoint creates a service account key",
|
||||
Request: new(serviceaccounttypes.PostableFactorAPIKey),
|
||||
RequestContentType: "",
|
||||
Response: new(serviceaccounttypes.GettableFactorAPIKeyWithKey),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
},
|
||||
handler.WithAuditDef(
|
||||
handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesFactorAPIKey,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.ResponseJSONPath("data.id"),
|
||||
},
|
||||
handler.AttachAuditDef{
|
||||
AttachedResource: coretypes.ResourceMetaResourceFactorAPIKey,
|
||||
AttachedResourceID: handler.ResponseJSONPath("data.id"),
|
||||
TargetResource: coretypes.ResourceServiceAccount,
|
||||
TargetResourceID: handler.PathParam("id"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
},
|
||||
),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/service_accounts/me", handler.New(provider.authzMiddleware.OpenAccess(provider.serviceAccountHandler.UpdateMe), handler.OpenAPIDef{
|
||||
ID: "UpdateMyServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Updates my service account",
|
||||
Description: "This endpoint gets my service account",
|
||||
Request: new(serviceaccounttypes.UpdatableServiceAccount),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: nil,
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Update, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "UpdateServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Updates a service account",
|
||||
Description: "This endpoint updates an existing service account",
|
||||
Request: new(serviceaccounttypes.UpdatableServiceAccount),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Delete, authtypes.Relation{Verb: coretypes.VerbDelete}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "DeleteServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Deletes a service account",
|
||||
Description: "This endpoint deletes an existing service account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbDelete)}),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.CreateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccountKey",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Create a service account key",
|
||||
Description: "This endpoint creates a service account key",
|
||||
Request: new(serviceaccounttypes.PostableFactorAPIKey),
|
||||
RequestContentType: "",
|
||||
Response: new(serviceaccounttypes.GettableFactorAPIKeyWithKey),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -328,59 +251,41 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(
|
||||
provider.authzMiddleware.Check(provider.serviceAccountHandler.UpdateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateServiceAccountKey",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Updates a service account key",
|
||||
Description: "This endpoint updates an existing service account key",
|
||||
Request: new(serviceaccounttypes.UpdatableFactorAPIKey),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("fid"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.UpdateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "UpdateServiceAccountKey",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Updates a service account key",
|
||||
Description: "This endpoint updates an existing service account key",
|
||||
Request: new(serviceaccounttypes.UpdatableFactorAPIKey),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(
|
||||
provider.authzMiddleware.Check(provider.serviceAccountHandler.RevokeFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}),
|
||||
handler.OpenAPIDef{
|
||||
ID: "RevokeServiceAccountKey",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Revoke a service account key",
|
||||
Description: "This endpoint revokes an existing service account key",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.RevokeFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "RevokeServiceAccountKey",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Revoke a service account key",
|
||||
Description: "This endpoint revokes an existing service account key",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,35 +4,25 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addSessionRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/sessions/email_password", handler.New(
|
||||
provider.authzMiddleware.OpenAccess(provider.sessionHandler.CreateSessionByEmailPassword),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateSessionByEmailPassword",
|
||||
Tags: []string{"sessions"},
|
||||
Summary: "Create session by email and password",
|
||||
Description: "This endpoint creates a session for a user using email and password.",
|
||||
Request: new(authtypes.PostableEmailPasswordSession),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(authtypes.GettableToken),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesSession,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/sessions/email_password", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.CreateSessionByEmailPassword), handler.OpenAPIDef{
|
||||
ID: "CreateSessionByEmailPassword",
|
||||
Tags: []string{"sessions"},
|
||||
Summary: "Create session by email and password",
|
||||
Description: "This endpoint creates a session for a user using email and password.",
|
||||
Request: new(authtypes.PostableEmailPasswordSession),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(authtypes.GettableToken),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -53,53 +43,37 @@ func (provider *provider) addSessionRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/sessions/rotate", handler.New(
|
||||
provider.authzMiddleware.OpenAccess(provider.sessionHandler.RotateSession),
|
||||
handler.OpenAPIDef{
|
||||
ID: "RotateSession",
|
||||
Tags: []string{"sessions"},
|
||||
Summary: "Rotate session",
|
||||
Description: "This endpoint rotates the session",
|
||||
Request: new(authtypes.PostableRotateToken),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(authtypes.GettableToken),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceSession,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/sessions/rotate", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.RotateSession), handler.OpenAPIDef{
|
||||
ID: "RotateSession",
|
||||
Tags: []string{"sessions"},
|
||||
Summary: "Rotate session",
|
||||
Description: "This endpoint rotates the session",
|
||||
Request: new(authtypes.PostableRotateToken),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(authtypes.GettableToken),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/sessions", handler.New(
|
||||
provider.authzMiddleware.OpenAccess(provider.sessionHandler.DeleteSession),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteSession",
|
||||
Tags: []string{"sessions"},
|
||||
Summary: "Delete session",
|
||||
Description: "This endpoint deletes the session",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceSession,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/sessions", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.DeleteSession), handler.OpenAPIDef{
|
||||
ID: "DeleteSession",
|
||||
Tags: []string{"sessions"},
|
||||
Summary: "Delete session",
|
||||
Description: "This endpoint deletes the session",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -49,12 +47,6 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesSpanMapperGroup,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.ResponseJSONPath("data.id"),
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -73,12 +65,6 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceSpanMapperGroup,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("groupId"),
|
||||
}),
|
||||
)).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -99,12 +85,6 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceSpanMapperGroup,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("groupId"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -145,22 +125,6 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(
|
||||
handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourcesSpanMapper,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.ResponseJSONPath("data.id"),
|
||||
},
|
||||
handler.AttachAuditDef{
|
||||
AttachedResource: coretypes.ResourceMetaResourceSpanMapper,
|
||||
AttachedResourceID: handler.ResponseJSONPath("data.id"),
|
||||
TargetResource: coretypes.ResourceMetaResourceSpanMapperGroup,
|
||||
TargetResourceID: handler.PathParam("groupId"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
},
|
||||
),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -179,12 +143,6 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceSpanMapper,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("mapperId"),
|
||||
}),
|
||||
)).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,12 +163,6 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceSpanMapper,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("mapperId"),
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -113,28 +111,20 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/me", handler.New(
|
||||
provider.authzMiddleware.OpenAccess(provider.userHandler.UpdateMyUser),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateMyUserV2",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Update my user v2",
|
||||
Description: "This endpoint updates the user I belong to",
|
||||
Request: new(types.UpdatableUser),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceUser,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/users/me", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.UpdateMyUser), handler.OpenAPIDef{
|
||||
ID: "UpdateMyUserV2",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Update my user v2",
|
||||
Description: "This endpoint updates the user I belong to",
|
||||
Request: new(types.UpdatableUser),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -189,29 +179,20 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.userHandler.UpdateUser),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateUser",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Update user v2",
|
||||
Description: "This endpoint updates the user by id",
|
||||
Request: new(types.UpdatableUser),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceUser,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryConfigurationChange,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
|
||||
ID: "UpdateUser",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Update user v2",
|
||||
Description: "This endpoint updates the user by id",
|
||||
Request: new(types.UpdatableUser),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -266,29 +247,20 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}/reset_password_tokens", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.userHandler.CreateResetPasswordToken),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateResetPasswordToken",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Create or regenerate reset password token for a user",
|
||||
Description: "This endpoint creates or regenerates a reset password token for a user. If a valid token exists, it is returned. If expired, a new one is created.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(types.ResetPasswordToken),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceFactorPassword,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
ResourceID: handler.PathParam("id"),
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/users/{id}/reset_password_tokens", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateResetPasswordToken), handler.OpenAPIDef{
|
||||
ID: "CreateResetPasswordToken",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Create or regenerate reset password token for a user",
|
||||
Description: "This endpoint creates or regenerates a reset password token for a user. If a valid token exists, it is returned. If expired, a new one is created.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(types.ResetPasswordToken),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -309,53 +281,37 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/me/factor_password", handler.New(
|
||||
provider.authzMiddleware.OpenAccess(provider.userHandler.ChangePassword),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateMyPassword",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Updates my password",
|
||||
Description: "This endpoint updates the password of the user I belong to",
|
||||
Request: new(types.ChangePasswordRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceFactorPassword,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/users/me/factor_password", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ChangePassword), handler.OpenAPIDef{
|
||||
ID: "UpdateMyPassword",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Updates my password",
|
||||
Description: "This endpoint updates the password of the user I belong to",
|
||||
Request: new(types.ChangePasswordRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/factor_password/forgot", handler.New(
|
||||
provider.authzMiddleware.OpenAccess(provider.userHandler.ForgotPassword),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ForgotPassword",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Forgot password",
|
||||
Description: "This endpoint initiates the forgot password flow by sending a reset password email",
|
||||
Request: new(types.PostableForgotPassword),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnprocessableEntity},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceFactorPassword,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/factor_password/forgot", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ForgotPassword), handler.OpenAPIDef{
|
||||
ID: "ForgotPassword",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Forgot password",
|
||||
Description: "This endpoint initiates the forgot password flow by sending a reset password email",
|
||||
Request: new(types.PostableForgotPassword),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnprocessableEntity},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -376,59 +332,37 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.userHandler.SetRoleByUserID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "SetRoleByUserID",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Set user roles",
|
||||
Description: "This endpoint assigns the role to the user roles by user id",
|
||||
Request: new(types.PostableRole),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.AttachAuditDef{
|
||||
AttachedResource: coretypes.ResourceRole,
|
||||
AttachedResourceID: handler.BodyJSONPath("name"),
|
||||
TargetResource: coretypes.ResourceUser,
|
||||
TargetResourceID: handler.PathParam("id"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.SetRoleByUserID), handler.OpenAPIDef{
|
||||
ID: "SetRoleByUserID",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Set user roles",
|
||||
Description: "This endpoint assigns the role to the user roles by user id",
|
||||
Request: new(types.PostableRole),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}/roles/{roleId}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.userHandler.RemoveUserRoleByRoleID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "RemoveUserRoleByUserIDAndRoleID",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Remove a role from user",
|
||||
Description: "This endpoint removes a role from the user by user id and role id",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.AttachAuditDef{
|
||||
AttachedResource: coretypes.ResourceRole,
|
||||
AttachedResourceID: handler.PathParam("roleId"),
|
||||
TargetResource: coretypes.ResourceUser,
|
||||
TargetResourceID: handler.PathParam("id"),
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: audittypes.ActionCategoryAccessControl,
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/users/{id}/roles/{roleId}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.RemoveUserRoleByRoleID), handler.OpenAPIDef{
|
||||
ID: "RemoveUserRoleByUserIDAndRoleID",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Remove a role from user",
|
||||
Description: "This endpoint removes a role from the user by user id and role id",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,35 +5,25 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/zeus/profiles", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutProfile),
|
||||
handler.OpenAPIDef{
|
||||
ID: "PutProfile",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Put profile in Zeus for a deployment.",
|
||||
Description: "This endpoint saves the profile of a deployment to zeus.",
|
||||
Request: new(zeustypes.PostableProfile),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceZeusProfile,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategorySystemEvent,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/zeus/profiles", handler.New(provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutProfile), handler.OpenAPIDef{
|
||||
ID: "PutProfile",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Put profile in Zeus for a deployment.",
|
||||
Description: "This endpoint saves the profile of a deployment to zeus.",
|
||||
Request: new(zeustypes.PostableProfile),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -54,28 +44,20 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutHost),
|
||||
handler.OpenAPIDef{
|
||||
ID: "PutHost",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Put host in Zeus for a deployment.",
|
||||
Description: "This endpoint saves the host of a deployment to zeus.",
|
||||
Request: new(zeustypes.PostableHost),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
handler.WithAuditDef(handler.BasicAuditDef{
|
||||
Resource: coretypes.ResourceMetaResourceZeusHost,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: audittypes.ActionCategorySystemEvent,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutHost), handler.OpenAPIDef{
|
||||
ID: "PutHost",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Put host in Zeus for a deployment.",
|
||||
Description: "This endpoint saves the host of a deployment to zeus.",
|
||||
Request: new(zeustypes.PostableHost),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
224
pkg/auditor/auditorserver/server_test.go
Normal file
224
pkg/auditor/auditorserver/server_test.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package auditorserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestSettings() factory.ScopedProviderSettings {
|
||||
return factory.NewScopedProviderSettings(instrumentationtest.New().ToProviderSettings(), "auditorserver_test")
|
||||
}
|
||||
|
||||
func newTestEvent(resource string, action coretypes.Verb) audittypes.AuditEvent {
|
||||
return audittypes.AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
EventName: audittypes.NewEventName(coretypes.MustNewKind(resource), action),
|
||||
AuditAttributes: audittypes.AuditAttributes{
|
||||
Action: action,
|
||||
Outcome: audittypes.OutcomeSuccess,
|
||||
},
|
||||
ResourceAttributes: audittypes.ResourceAttributes{
|
||||
ResourceKind: coretypes.MustNewKind(resource),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
settings := newTestSettings()
|
||||
config := Config{BufferSize: 10, BatchSize: 5, FlushInterval: time.Second}
|
||||
|
||||
server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { return nil })
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, server)
|
||||
}
|
||||
|
||||
func TestStart_Stop(t *testing.T) {
|
||||
settings := newTestSettings()
|
||||
config := Config{BufferSize: 10, BatchSize: 5, FlushInterval: time.Second}
|
||||
|
||||
server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { return nil })
|
||||
require.NoError(t, err)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- server.Start(context.Background()) }()
|
||||
|
||||
require.NoError(t, server.Stop(context.Background()))
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
assert.NoError(t, err)
|
||||
case <-time.After(2 * time.Second):
|
||||
assert.Fail(t, "Start did not return after Stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd_FlushesOnBatchSize(t *testing.T) {
|
||||
var exported []audittypes.AuditEvent
|
||||
var mu sync.Mutex
|
||||
|
||||
settings := newTestSettings()
|
||||
config := Config{BufferSize: 100, BatchSize: 3, FlushInterval: time.Hour}
|
||||
|
||||
server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error {
|
||||
mu.Lock()
|
||||
exported = append(exported, events...)
|
||||
mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() { _ = server.Start(ctx) }()
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbCreate))
|
||||
}
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return len(exported) == 3
|
||||
}, 2*time.Second, 10*time.Millisecond)
|
||||
|
||||
require.NoError(t, server.Stop(ctx))
|
||||
}
|
||||
|
||||
func TestAdd_FlushesOnInterval(t *testing.T) {
|
||||
var exported atomic.Int64
|
||||
|
||||
settings := newTestSettings()
|
||||
config := Config{BufferSize: 100, BatchSize: 1000, FlushInterval: 50 * time.Millisecond}
|
||||
|
||||
server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error {
|
||||
exported.Add(int64(len(events)))
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() { _ = server.Start(ctx) }()
|
||||
|
||||
server.Add(ctx, newTestEvent("user", coretypes.VerbUpdate))
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return exported.Load() == 1
|
||||
}, 2*time.Second, 10*time.Millisecond)
|
||||
|
||||
require.NoError(t, server.Stop(ctx))
|
||||
}
|
||||
|
||||
func TestAdd_DropsWhenBufferFull(t *testing.T) {
|
||||
settings := newTestSettings()
|
||||
config := Config{BufferSize: 2, BatchSize: 100, FlushInterval: time.Hour}
|
||||
|
||||
server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { return nil })
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbCreate))
|
||||
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbUpdate))
|
||||
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbDelete))
|
||||
|
||||
assert.Equal(t, 2, server.queueLen())
|
||||
}
|
||||
|
||||
func TestStop_DrainsRemainingEvents(t *testing.T) {
|
||||
var exported atomic.Int64
|
||||
|
||||
settings := newTestSettings()
|
||||
config := Config{BufferSize: 100, BatchSize: 100, FlushInterval: time.Hour}
|
||||
|
||||
server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error {
|
||||
exported.Add(int64(len(events)))
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() { _ = server.Start(ctx) }()
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
server.Add(ctx, newTestEvent("alert-rule", coretypes.VerbCreate))
|
||||
}
|
||||
|
||||
require.NoError(t, server.Stop(ctx))
|
||||
|
||||
assert.Equal(t, int64(5), exported.Load())
|
||||
}
|
||||
|
||||
func TestAdd_ContinuesAfterExportFailure(t *testing.T) {
|
||||
var calls atomic.Int64
|
||||
|
||||
settings := newTestSettings()
|
||||
config := Config{BufferSize: 100, BatchSize: 2, FlushInterval: time.Hour}
|
||||
|
||||
server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error {
|
||||
calls.Add(1)
|
||||
return errors.New(errors.TypeInternal, errors.CodeInternal, "connection refused")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() { _ = server.Start(ctx) }()
|
||||
|
||||
server.Add(ctx, newTestEvent("user", coretypes.VerbDelete))
|
||||
server.Add(ctx, newTestEvent("user", coretypes.VerbDelete))
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return calls.Load() >= 1
|
||||
}, 2*time.Second, 10*time.Millisecond)
|
||||
|
||||
require.NoError(t, server.Stop(ctx))
|
||||
}
|
||||
|
||||
func TestAdd_ConcurrentSafety(t *testing.T) {
|
||||
var exported atomic.Int64
|
||||
|
||||
settings := newTestSettings()
|
||||
config := Config{BufferSize: 1000, BatchSize: 10, FlushInterval: 50 * time.Millisecond}
|
||||
|
||||
server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error {
|
||||
exported.Add(int64(len(events)))
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() { _ = server.Start(ctx) }()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbCreate))
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
require.NoError(t, server.Stop(ctx))
|
||||
|
||||
assert.Equal(t, int64(100), exported.Load())
|
||||
}
|
||||
@@ -15,13 +15,13 @@ type ServeOpenAPIFunc func(openapi.OperationContext)
|
||||
type Handler interface {
|
||||
http.Handler
|
||||
ServeOpenAPI(openapi.OperationContext)
|
||||
AuditDefs() []AuditDef
|
||||
AuditDef() *AuditDef
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
handlerFunc http.HandlerFunc
|
||||
openAPIDef OpenAPIDef
|
||||
auditDefs []AuditDef
|
||||
auditDef *AuditDef
|
||||
}
|
||||
|
||||
func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef, opts ...Option) Handler {
|
||||
@@ -130,6 +130,6 @@ func (handler *handler) ServeOpenAPI(opCtx openapi.OperationContext) {
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *handler) AuditDefs() []AuditDef {
|
||||
return handler.auditDefs
|
||||
func (handler *handler) AuditDef() *AuditDef {
|
||||
return handler.auditDef
|
||||
}
|
||||
|
||||
@@ -1,139 +1,25 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// Option configures optional behaviour on a handler created by New.
|
||||
type Option func(*handler)
|
||||
|
||||
// ExtractorContext carries everything a ResourceIDExtractor might read out of
|
||||
// a request/response cycle. The middleware pre-buffers the request body and
|
||||
// captures the response body so post-handler extraction works on both sides.
|
||||
type ExtractorContext struct {
|
||||
Request *http.Request
|
||||
RequestBody []byte
|
||||
ResponseBody []byte
|
||||
type AuditDef struct {
|
||||
ResourceKind coretypes.Kind // Typeable.Kind() value, e.g. "dashboard", "user".
|
||||
Action coretypes.Verb // create, update, delete, etc.
|
||||
Category audittypes.ActionCategory // access_control, configuration_change, etc.
|
||||
ResourceIDParam string // Gorilla mux path param name for the resource ID.
|
||||
}
|
||||
|
||||
// ResourceIDExtractor pulls a resource id from an incoming request and/or its
|
||||
// response. Returns an empty string with no error when the id source is
|
||||
// genuinely absent (e.g. "me" routes that act on the caller without an id).
|
||||
type ResourceIDExtractor func(ExtractorContext) (string, error)
|
||||
|
||||
// ResourceIDsExtractor pulls a list of resource ids. Used by AttachManyAuditDef
|
||||
// to fan out one audit event per attached entity referenced in a request body.
|
||||
type ResourceIDsExtractor func(ExtractorContext) ([]string, error)
|
||||
|
||||
// PathParam returns an extractor that reads a Gorilla mux path variable.
|
||||
func PathParam(name string) ResourceIDExtractor {
|
||||
return func(ctx ExtractorContext) (string, error) {
|
||||
vars := mux.Vars(ctx.Request)
|
||||
if vars == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return vars[name], nil
|
||||
}
|
||||
}
|
||||
|
||||
// BodyJSONPath returns an extractor that reads a JSON path from the request
|
||||
// body via gjson. The middleware buffers the request body before forwarding
|
||||
// to the handler, so this extractor still works after the handler runs.
|
||||
func BodyJSONPath(path string) ResourceIDExtractor {
|
||||
return func(ctx ExtractorContext) (string, error) {
|
||||
return gjson.GetBytes(ctx.RequestBody, path).String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseJSONPath returns an extractor that reads a JSON path from the
|
||||
// response body via gjson. Useful for Create routes where the new resource id
|
||||
// is only known after the handler runs and writes the response payload.
|
||||
func ResponseJSONPath(path string) ResourceIDExtractor {
|
||||
return func(ctx ExtractorContext) (string, error) {
|
||||
return gjson.GetBytes(ctx.ResponseBody, path).String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
// BodyJSONArray returns a multi-id extractor that reads a JSON array of
|
||||
// strings out of the request body at the given gjson path.
|
||||
func BodyJSONArray(path string) ResourceIDsExtractor {
|
||||
return func(ctx ExtractorContext) ([]string, error) {
|
||||
result := gjson.GetBytes(ctx.RequestBody, path)
|
||||
if !result.Exists() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
array := result.Array()
|
||||
ids := make([]string, 0, len(array))
|
||||
for _, r := range array {
|
||||
ids = append(ids, r.String())
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
}
|
||||
|
||||
// AuditDef is a sealed interface implemented by BasicAuditDef and
|
||||
// AttachAuditDef. The middleware type-switches over its implementations to
|
||||
// build the audit event for the matched route.
|
||||
type AuditDef interface {
|
||||
sealAuditDef()
|
||||
}
|
||||
|
||||
// BasicAuditDef declares audit metadata for routes that operate on a single
|
||||
// resource. EventName is derived as Resource.Kind() + "." + Verb.PastTense().
|
||||
type BasicAuditDef struct {
|
||||
Resource coretypes.Resource
|
||||
Verb coretypes.Verb
|
||||
Category audittypes.ActionCategory
|
||||
ResourceID ResourceIDExtractor // nil for collection routes with no addressable id
|
||||
}
|
||||
|
||||
func (BasicAuditDef) sealAuditDef() {}
|
||||
|
||||
// AttachAuditDef declares audit metadata for routes that attach one resource
|
||||
// to another (e.g. role attached to a user). The event subject is the
|
||||
// attached resource; the target carries where it was attached. EventName is
|
||||
// derived as AttachedResource.Kind() + "." + Verb.PastTense().
|
||||
type AttachAuditDef struct {
|
||||
AttachedResource coretypes.Resource
|
||||
AttachedResourceID ResourceIDExtractor
|
||||
TargetResource coretypes.Resource
|
||||
TargetResourceID ResourceIDExtractor
|
||||
Verb coretypes.Verb
|
||||
Category audittypes.ActionCategory
|
||||
}
|
||||
|
||||
func (AttachAuditDef) sealAuditDef() {}
|
||||
|
||||
// AttachManyAuditDef declares that a single request attaches many of the same
|
||||
// kind of resource to one target. The middleware fans out one attach event per
|
||||
// id returned by AttachedResourceIDs. Used for routes whose body carries a
|
||||
// list of references (e.g. rule preferredChannels, planned-maintenance
|
||||
// alertIds, route-policy channels).
|
||||
type AttachManyAuditDef struct {
|
||||
AttachedResource coretypes.Resource
|
||||
AttachedResourceIDs ResourceIDsExtractor
|
||||
TargetResource coretypes.Resource
|
||||
TargetResourceID ResourceIDExtractor
|
||||
Verb coretypes.Verb
|
||||
Category audittypes.ActionCategory
|
||||
}
|
||||
|
||||
func (AttachManyAuditDef) sealAuditDef() {}
|
||||
|
||||
// WithAuditDef attaches one or more AuditDef declarations to the handler. A
|
||||
// single route can produce multiple audit events — e.g. creating a resource
|
||||
// that is simultaneously attached to a parent emits one BasicAuditDef and one
|
||||
// AttachAuditDef. The middleware emits one event per def in declaration order.
|
||||
func WithAuditDef(defs ...AuditDef) Option {
|
||||
// WithAudit attaches an AuditDef to the handler. The actual audit event
|
||||
// emission is handled by the middleware layer, which reads the AuditDef
|
||||
// from the matched route's handler.
|
||||
func WithAuditDef(def AuditDef) Option {
|
||||
return func(h *handler) {
|
||||
h.auditDefs = append(h.auditDefs, defs...)
|
||||
h.auditDef = &def
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -18,7 +16,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -62,14 +59,6 @@ func (middleware *Audit) Wrap(next http.Handler) http.Handler {
|
||||
string(semconv.HTTPRouteKey), path,
|
||||
}
|
||||
|
||||
// Pre-buffer the request body if the route declares any AuditDefs that
|
||||
// might want to extract from it after the handler has consumed the body.
|
||||
var requestBody []byte
|
||||
if len(auditDefsFromRequest(req)) > 0 && req.Body != nil {
|
||||
requestBody, _ = io.ReadAll(req.Body)
|
||||
req.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
}
|
||||
|
||||
responseBuffer := &byteBuffer{}
|
||||
writer := newResponseCapture(rw, responseBuffer)
|
||||
next.ServeHTTP(writer, req)
|
||||
@@ -81,7 +70,7 @@ func (middleware *Audit) Wrap(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.emitAuditEvent(req, writer, path, requestBody)
|
||||
middleware.emitAuditEvent(req, writer, path)
|
||||
|
||||
fields = append(fields,
|
||||
string(semconv.HTTPResponseStatusCodeKey), statusCode,
|
||||
@@ -100,72 +89,51 @@ func (middleware *Audit) Wrap(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCapture, routeTemplate string, requestBody []byte) {
|
||||
func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCapture, routeTemplate string) {
|
||||
if middleware.auditor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
defs := auditDefsFromRequest(req)
|
||||
if len(defs) == 0 {
|
||||
def := auditDefFromRequest(req)
|
||||
if def == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// extract claims
|
||||
claims, _ := authtypes.ClaimsFromContext(req.Context())
|
||||
|
||||
// extract status code
|
||||
statusCode := writer.StatusCode()
|
||||
|
||||
// extract traces.
|
||||
span := trace.SpanFromContext(req.Context())
|
||||
|
||||
// extract error details.
|
||||
var errorType, errorCode string
|
||||
if statusCode >= 400 {
|
||||
errorType = render.ErrorTypeFromStatusCode(statusCode)
|
||||
errorCode = render.ErrorCodeFromBody(writer.BodyBytes())
|
||||
}
|
||||
|
||||
extractorCtx := handler.ExtractorContext{
|
||||
Request: req,
|
||||
RequestBody: requestBody,
|
||||
ResponseBody: writer.BodyBytes(),
|
||||
}
|
||||
event := audittypes.NewAuditEventFromHTTPRequest(
|
||||
req,
|
||||
routeTemplate,
|
||||
statusCode,
|
||||
span.SpanContext().TraceID(),
|
||||
span.SpanContext().SpanID(),
|
||||
def.Action,
|
||||
def.Category,
|
||||
claims,
|
||||
resourceIDFromRequest(req, def.ResourceIDParam),
|
||||
def.ResourceKind,
|
||||
errorType,
|
||||
errorCode,
|
||||
)
|
||||
|
||||
for _, def := range defs {
|
||||
resolved, err := resolveAuditDef(extractorCtx, def)
|
||||
if err != nil {
|
||||
middleware.logger.WarnContext(req.Context(), "audit event dropped — resource id extraction failed", errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
if len(resolved) == 0 {
|
||||
if _, attach := def.(handler.AttachManyAuditDef); attach {
|
||||
middleware.logger.WarnContext(req.Context(), "audit AttachManyAuditDef resolved to zero events", slog.Int("request_body_size", len(requestBody)), slog.String("request_body_head", truncate(requestBody, 256)))
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range resolved {
|
||||
event := audittypes.NewAuditEventFromHTTPRequest(
|
||||
req,
|
||||
routeTemplate,
|
||||
statusCode,
|
||||
span.SpanContext().TraceID(),
|
||||
span.SpanContext().SpanID(),
|
||||
r.Verb,
|
||||
r.Category,
|
||||
claims,
|
||||
r.ResourceAttributes,
|
||||
errorType,
|
||||
errorCode,
|
||||
)
|
||||
|
||||
middleware.auditor.Audit(req.Context(), event)
|
||||
}
|
||||
}
|
||||
middleware.auditor.Audit(req.Context(), event)
|
||||
}
|
||||
|
||||
type resolvedAuditDef struct {
|
||||
Verb coretypes.Verb
|
||||
Category audittypes.ActionCategory
|
||||
ResourceAttributes audittypes.ResourceAttributes
|
||||
}
|
||||
|
||||
func auditDefsFromRequest(req *http.Request) []handler.AuditDef {
|
||||
func auditDefFromRequest(req *http.Request) *handler.AuditDef {
|
||||
route := mux.CurrentRoute(req)
|
||||
if route == nil {
|
||||
return nil
|
||||
@@ -184,84 +152,18 @@ func auditDefsFromRequest(req *http.Request) []handler.AuditDef {
|
||||
return nil
|
||||
}
|
||||
|
||||
return provider.AuditDefs()
|
||||
return provider.AuditDef()
|
||||
}
|
||||
|
||||
func resolveAuditDef(ctx handler.ExtractorContext, def handler.AuditDef) ([]resolvedAuditDef, error) {
|
||||
switch d := def.(type) {
|
||||
case handler.BasicAuditDef:
|
||||
resourceID, err := extractResourceID(ctx, d.ResourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []resolvedAuditDef{{
|
||||
Verb: d.Verb,
|
||||
Category: d.Category,
|
||||
ResourceAttributes: audittypes.NewResourceAttributes(d.Resource, resourceID),
|
||||
}}, nil
|
||||
case handler.AttachAuditDef:
|
||||
attachedID, err := extractResourceID(ctx, d.AttachedResourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targetID, err := extractResourceID(ctx, d.TargetResourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []resolvedAuditDef{{
|
||||
Verb: d.Verb,
|
||||
Category: d.Category,
|
||||
ResourceAttributes: audittypes.NewAttachResourceAttributes(d.AttachedResource, attachedID, d.TargetResource, targetID),
|
||||
}}, nil
|
||||
case handler.AttachManyAuditDef:
|
||||
ids, err := extractResourceIDs(ctx, d.AttachedResourceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targetID, err := extractResourceID(ctx, d.TargetResourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolved := make([]resolvedAuditDef, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
resolved = append(resolved, resolvedAuditDef{
|
||||
Verb: d.Verb,
|
||||
Category: d.Category,
|
||||
ResourceAttributes: audittypes.NewAttachResourceAttributes(d.AttachedResource, id, d.TargetResource, targetID),
|
||||
})
|
||||
}
|
||||
|
||||
return resolved, nil
|
||||
func resourceIDFromRequest(req *http.Request, param string) string {
|
||||
if param == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "unknown AuditDef implementation %T", def)
|
||||
}
|
||||
|
||||
func extractResourceID(ctx handler.ExtractorContext, extractor handler.ResourceIDExtractor) (string, error) {
|
||||
if extractor == nil {
|
||||
return "", nil
|
||||
vars := mux.Vars(req)
|
||||
if vars == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return extractor(ctx)
|
||||
}
|
||||
|
||||
func extractResourceIDs(ctx handler.ExtractorContext, extractor handler.ResourceIDsExtractor) ([]string, error) {
|
||||
if extractor == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return extractor(ctx)
|
||||
}
|
||||
|
||||
func truncate(b []byte, n int) string {
|
||||
if len(b) <= n {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
return string(b[:n]) + "..."
|
||||
return vars[param]
|
||||
}
|
||||
|
||||
140
pkg/modules/inframonitoring/implinframonitoring/clusters.go
Normal file
140
pkg/modules/inframonitoring/implinframonitoring/clusters.go
Normal file
@@ -0,0 +1,140 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -93,3 +93,51 @@ func (h *handler) ListNodes(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) ListNamespaces(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.PostableNamespaces
|
||||
if err := binding.JSON.BindBody(req.Body, &parsedReq); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.ListNamespaces(req.Context(), orgID, &parsedReq)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -337,3 +337,194 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNamespaces) (*inframonitoringtypes.Namespaces, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &inframonitoringtypes.Namespaces{}
|
||||
|
||||
if req.OrderBy == nil {
|
||||
req.OrderBy = &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: inframonitoringtypes.NamespacesOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []qbtypes.GroupByKey{namespaceNameGroupByKey}
|
||||
resp.Type = inframonitoringtypes.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, namespacesTableMetricNamesList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(missingMetrics) > 0 {
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
|
||||
resp.Records = []inframonitoringtypes.NamespaceRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
if req.End < int64(minFirstReportedUnixMilli) {
|
||||
resp.EndTimeBeforeRetention = true
|
||||
resp.Records = []inframonitoringtypes.NamespaceRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
|
||||
|
||||
metadataMap, err := m.getNamespacesTableMetadata(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
pageGroups, err := m.getTopNamespaceGroups(ctx, orgID, req, metadataMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
resp.Records = []inframonitoringtypes.NamespaceRecord{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNamespacesTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostablePods.
|
||||
phaseCounts, 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 = buildNamespaceRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
123
pkg/modules/inframonitoring/implinframonitoring/namespaces.go
Normal file
123
pkg/modules/inframonitoring/implinframonitoring/namespaces.go
Normal file
@@ -0,0 +1,123 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// buildNamespaceRecords assembles the page records. Pod phase counts come from
|
||||
// phaseCounts in both modes; every row is a group of pods, so there's no
|
||||
// per-row "current phase" concept (unlike pods/nodes list mode).
|
||||
func buildNamespaceRecords(
|
||||
resp *qbtypes.QueryRangeResponse,
|
||||
pageGroups []map[string]string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
metadataMap map[string]map[string]string,
|
||||
phaseCounts map[string]podPhaseCounts,
|
||||
) []inframonitoringtypes.NamespaceRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
records := make([]inframonitoringtypes.NamespaceRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
namespaceName := labels[namespaceNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.NamespaceRecord{ // initialize with default values
|
||||
NamespaceName: namespaceName,
|
||||
NamespaceCPU: -1,
|
||||
NamespaceMemory: -1,
|
||||
Meta: map[string]string{},
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
if v, exists := metrics["A"]; exists {
|
||||
record.NamespaceCPU = v
|
||||
}
|
||||
if v, exists := metrics["D"]; exists {
|
||||
record.NamespaceMemory = v
|
||||
}
|
||||
}
|
||||
|
||||
if phaseCountsForGroup, ok := phaseCounts[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) getTopNamespaceGroups(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableNamespaces,
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
queryNamesForOrderBy := orderByToNamespacesQueryNames[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.newNamespacesTableListQuery().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) getNamespacesTableMetadata(ctx context.Context, req *inframonitoringtypes.PostableNamespaces) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range namespaceAttrKeysForMetadata {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, namespacesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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"
|
||||
)
|
||||
|
||||
const (
|
||||
namespaceNameAttrKey = "k8s.namespace.name"
|
||||
)
|
||||
|
||||
var namespaceNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: namespaceNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
}
|
||||
|
||||
// namespacesTableMetricNamesList drives the existence/retention check.
|
||||
// Includes k8s.pod.phase so the response short-circuits cleanly when a
|
||||
// cluster doesn't ship the metric — even though phase isn't part of the
|
||||
// QB composite query (it's queried separately via getPerGroupPodPhaseCounts).
|
||||
var namespacesTableMetricNamesList = []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
"k8s.pod.phase",
|
||||
}
|
||||
|
||||
var namespaceAttrKeysForMetadata = []string{
|
||||
"k8s.namespace.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
var orderByToNamespacesQueryNames = map[string][]string{
|
||||
inframonitoringtypes.NamespacesOrderByCPU: {"A"},
|
||||
inframonitoringtypes.NamespacesOrderByMemory: {"D"},
|
||||
}
|
||||
|
||||
// newNamespacesTableListQuery builds the composite QB v5 request for the namespaces list.
|
||||
// Pod phase counts are derived separately via getPerGroupPodPhaseCounts (works for both
|
||||
// list and grouped_list modes), so no phase query is included here.
|
||||
// Query letters A and D are kept aligned with the v1 implementation.
|
||||
func (m *module) newNamespacesTableListQuery() *qbtypes.QueryRangeRequest {
|
||||
queries := []qbtypes.QueryEnvelope{
|
||||
// Query A: CPU usage — sum of pod CPU within the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.cpu.usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{namespaceNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query D: Memory working set — sum of pod memory within the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "D",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.memory.working_set",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{namespaceNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: queries,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,14 @@ type Handler interface {
|
||||
ListHosts(http.ResponseWriter, *http.Request)
|
||||
ListPods(http.ResponseWriter, *http.Request)
|
||||
ListNodes(http.ResponseWriter, *http.Request)
|
||||
ListNamespaces(http.ResponseWriter, *http.Request)
|
||||
ListClusters(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
ListHosts(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableHosts) (*inframonitoringtypes.Hosts, error)
|
||||
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.GetWithRoles(ctx, orgID, id)
|
||||
serviceAccount, err := module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -387,24 +387,12 @@ func (module *module) setRole(ctx context.Context, orgID valuer.UUID, id valuer.
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.authz.ModifyGrant(ctx, orgID, serviceAccount.RoleNames(), []string{role.Name}, authtypes.MustNewSubject(coretypes.NewResourceServiceAccount(), id.String(), orgID, nil))
|
||||
err = module.authz.Grant(ctx, orgID, []string{role.Name}, authtypes.MustNewSubject(coretypes.NewResourceServiceAccount(), id.String(), orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
err = module.store.CreateServiceAccountRole(ctx, serviceAccountRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -207,21 +207,6 @@ 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.
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ func auditFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
||||
|
||||
return map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"service.name": {key("service.name", res, str, false)},
|
||||
"signoz.audit.verb": {key("signoz.audit.verb", attr, str, true)},
|
||||
"signoz.audit.action": {key("signoz.audit.action", attr, str, true)},
|
||||
"signoz.audit.outcome": {key("signoz.audit.outcome", attr, str, true)},
|
||||
"signoz.audit.principal.email": {key("signoz.audit.principal.email", attr, str, true)},
|
||||
"signoz.audit.principal.id": {key("signoz.audit.principal.id", attr, str, true)},
|
||||
@@ -131,20 +131,20 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", "019b-5678-efgh-9012", "%signoz.audit.resource.id%", "%signoz.audit.resource.id\":\"019b-5678-efgh-9012%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
// List: all dashboard deletions (compliance — resource.kind + verb AND)
|
||||
// List: all dashboard deletions (compliance — resource.kind + action AND)
|
||||
{
|
||||
name: "ListByResourceKindAndVerb",
|
||||
name: "ListByResourceKindAndAction",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.resource.kind = 'dashboard' AND signoz.audit.verb = 'delete'",
|
||||
Expression: "signoz.audit.resource.kind = 'dashboard' AND signoz.audit.action = 'delete'",
|
||||
},
|
||||
Limit: 100,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$verb` = ? AND `attribute_string_signoz$$audit$$verb_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "delete", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
@@ -165,23 +165,23 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Args: []any{"service_account", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
// Scalar: alert — count forbidden errors (outcome + verb AND)
|
||||
// Scalar: alert — count forbidden errors (outcome + action AND)
|
||||
{
|
||||
name: "ScalarCountByOutcomeAndVerb",
|
||||
name: "ScalarCountByOutcomeAndAction",
|
||||
requestType: qbtypes.RequestTypeScalar,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.outcome = 'failure' AND signoz.audit.verb = 'update'",
|
||||
Expression: "signoz.audit.outcome = 'failure' AND signoz.audit.action = 'update'",
|
||||
},
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT count() AS __result_0 FROM signoz_audit.distributed_logs WHERE ((`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = ?) AND (`attribute_string_signoz$$audit$$verb` = ? AND `attribute_string_signoz$$audit$$verb_exists` = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY __result_0 DESC",
|
||||
Query: "SELECT count() AS __result_0 FROM signoz_audit.distributed_logs WHERE ((`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = ?) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY __result_0 DESC",
|
||||
Args: []any{"failure", true, "update", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -413,28 +413,6 @@ func (b *traceOperatorCTEBuilder) buildFinalQuery(ctx context.Context, selectFro
|
||||
}
|
||||
|
||||
func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFromCTE string) (*qbtypes.Statement, error) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
// Select core fields
|
||||
sb.Select(
|
||||
"timestamp",
|
||||
"trace_id",
|
||||
"span_id",
|
||||
"name",
|
||||
"duration_nano",
|
||||
"parent_span_id",
|
||||
)
|
||||
|
||||
selectedFields := map[string]bool{
|
||||
"timestamp": true,
|
||||
"trace_id": true,
|
||||
"span_id": true,
|
||||
"name": true,
|
||||
"duration_nano": true,
|
||||
"parent_span_id": true,
|
||||
}
|
||||
|
||||
// Get keys for selectFields
|
||||
keySelectors := b.getKeySelectors()
|
||||
for _, field := range b.operator.SelectFields {
|
||||
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
|
||||
@@ -444,13 +422,38 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom
|
||||
FieldDataType: field.FieldDataType,
|
||||
})
|
||||
}
|
||||
|
||||
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add selectFields using ColumnExpressionFor since we now have all base table columns
|
||||
coreFields := []string{"timestamp", "trace_id", "span_id", "name", "duration_nano", "parent_span_id"}
|
||||
selectedFields := map[string]bool{
|
||||
"timestamp": true,
|
||||
"trace_id": true,
|
||||
"span_id": true,
|
||||
"name": true,
|
||||
"duration_nano": true,
|
||||
"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)
|
||||
}
|
||||
|
||||
innerSB := sqlbuilder.NewSelectBuilder()
|
||||
innerSB.Select(innerCoreExprs...)
|
||||
|
||||
var additionalSelectedFields []string
|
||||
for _, field := range b.operator.SelectFields {
|
||||
if selectedFields[field.Name] {
|
||||
continue
|
||||
@@ -461,41 +464,56 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom
|
||||
slog.String("field", field.Name), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
sb.SelectMore(colExpr)
|
||||
innerSB.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
|
||||
}
|
||||
sb.OrderBy(fmt.Sprintf("%s %s", colExpr, orderBy.Direction.StringValue()))
|
||||
orderApplied = true
|
||||
innerSB.SelectMore(colExpr)
|
||||
selectedFields[orderBy.Key.Name] = true
|
||||
}
|
||||
|
||||
if !orderApplied {
|
||||
sb.OrderBy("timestamp DESC")
|
||||
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 b.operator.Limit > 0 {
|
||||
sb.Limit(b.operator.Limit)
|
||||
outerSB.Limit(b.operator.Limit)
|
||||
} else {
|
||||
sb.Limit(100)
|
||||
outerSB.Limit(100)
|
||||
}
|
||||
|
||||
if b.operator.Offset > 0 {
|
||||
sb.Offset(b.operator.Offset)
|
||||
outerSB.Offset(b.operator.Offset)
|
||||
}
|
||||
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
outerSQL, outerArgs := outerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return &qbtypes.Statement{
|
||||
Query: sql,
|
||||
Args: args,
|
||||
Query: outerSQL,
|
||||
Args: append(innerArgs, outerArgs...),
|
||||
}, 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 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",
|
||||
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",
|
||||
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 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",
|
||||
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",
|
||||
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 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",
|
||||
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",
|
||||
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 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",
|
||||
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",
|
||||
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 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",
|
||||
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",
|
||||
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,11 +380,72 @@ 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 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",
|
||||
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",
|
||||
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()
|
||||
|
||||
@@ -11,22 +11,22 @@ import (
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
)
|
||||
|
||||
// Audit attributes — Verb (What).
|
||||
// Audit attributes — Action (What).
|
||||
type AuditAttributes struct {
|
||||
Verb coretypes.Verb // guaranteed to be present
|
||||
Action coretypes.Verb // guaranteed to be present
|
||||
ActionCategory ActionCategory // guaranteed to be present
|
||||
Outcome Outcome // guaranteed to be present
|
||||
IdentNProvider authtypes.IdentNProvider
|
||||
}
|
||||
|
||||
func NewAuditAttributesFromHTTP(statusCode int, verb coretypes.Verb, category ActionCategory, claims authtypes.Claims) AuditAttributes {
|
||||
func NewAuditAttributesFromHTTP(statusCode int, action coretypes.Verb, category ActionCategory, claims authtypes.Claims) AuditAttributes {
|
||||
outcome := OutcomeFailure
|
||||
if statusCode >= 200 && statusCode < 400 {
|
||||
outcome = OutcomeSuccess
|
||||
}
|
||||
|
||||
return AuditAttributes{
|
||||
Verb: verb,
|
||||
Action: action,
|
||||
ActionCategory: category,
|
||||
Outcome: outcome,
|
||||
IdentNProvider: claims.IdentNProvider,
|
||||
@@ -34,7 +34,7 @@ func NewAuditAttributesFromHTTP(statusCode int, verb coretypes.Verb, category Ac
|
||||
}
|
||||
|
||||
func (attributes AuditAttributes) Put(dest pcommon.Map) {
|
||||
dest.PutStr("signoz.audit.verb", attributes.Verb.StringValue())
|
||||
dest.PutStr("signoz.audit.action", attributes.Action.StringValue())
|
||||
dest.PutStr("signoz.audit.action_category", attributes.ActionCategory.StringValue())
|
||||
dest.PutStr("signoz.audit.outcome", attributes.Outcome.StringValue())
|
||||
putStrIfNotEmpty(dest, "signoz.audit.identn_provider", attributes.IdentNProvider.StringValue())
|
||||
@@ -70,47 +70,24 @@ func (attributes PrincipalAttributes) Put(dest pcommon.Map) {
|
||||
|
||||
// Audit attributes — Resource (On What).
|
||||
// These are OTel resource attributes (placed on the Resource, not event attributes).
|
||||
// For attach events, Target carries the resource the primary was attached to.
|
||||
type ResourceAttributes struct {
|
||||
Resource coretypes.Resource // guaranteed to be present
|
||||
ResourceID string
|
||||
TargetResource coretypes.Resource // present only for attach events
|
||||
TargetResourceID string
|
||||
ResourceID string
|
||||
ResourceKind coretypes.Kind // guaranteed to be present
|
||||
}
|
||||
|
||||
func NewResourceAttributes(resource coretypes.Resource, resourceID string) ResourceAttributes {
|
||||
func NewResourceAttributes(resourceID string, resourceKind coretypes.Kind) ResourceAttributes {
|
||||
return ResourceAttributes{
|
||||
Resource: resource,
|
||||
ResourceID: resourceID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAttachResourceAttributes(attachedResource coretypes.Resource, attachedResourceID string, targetResource coretypes.Resource, targetResourceID string) ResourceAttributes {
|
||||
return ResourceAttributes{
|
||||
Resource: attachedResource,
|
||||
ResourceID: attachedResourceID,
|
||||
TargetResource: targetResource,
|
||||
TargetResourceID: targetResourceID,
|
||||
ResourceID: resourceID,
|
||||
ResourceKind: resourceKind,
|
||||
}
|
||||
}
|
||||
|
||||
// PutResource writes the resource attributes to an OTel Resource's attribute map.
|
||||
// These are resource-level attributes (stored in the resource JSON column),
|
||||
// not event-level attributes (stored in attributes_string).
|
||||
func (attributes ResourceAttributes) PutResource(dest pcommon.Map, orgID valuer.UUID) {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.kind", attributes.Resource.Kind().String())
|
||||
func (attributes ResourceAttributes) PutResource(dest pcommon.Map) {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.kind", attributes.ResourceKind.String())
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.id", attributes.ResourceID)
|
||||
if attributes.ResourceID != "" {
|
||||
dest.PutStr("signoz.audit.resource.object", attributes.Resource.Object(orgID, attributes.ResourceID))
|
||||
}
|
||||
|
||||
if attributes.TargetResource != nil {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.target.kind", attributes.TargetResource.Kind().String())
|
||||
putStrIfNotEmpty(dest, "signoz.audit.target.id", attributes.TargetResourceID)
|
||||
if attributes.TargetResourceID != "" {
|
||||
dest.PutStr("signoz.audit.target.object", attributes.TargetResource.Object(orgID, attributes.TargetResourceID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audit attributes — Error (When outcome is failure)
|
||||
@@ -203,37 +180,26 @@ func newBody(auditAttributes AuditAttributes, principalAttributes PrincipalAttri
|
||||
b.WriteString(principalAttributes.PrincipalID.StringValue())
|
||||
}
|
||||
|
||||
// Verb: " created" or " failed to create".
|
||||
// Action: " created" or " failed to create".
|
||||
if b.Len() > 0 {
|
||||
b.WriteString(" ")
|
||||
}
|
||||
if auditAttributes.Outcome == OutcomeSuccess {
|
||||
b.WriteString(auditAttributes.Verb.PastTense())
|
||||
b.WriteString(auditAttributes.Action.PastTense())
|
||||
} else {
|
||||
b.WriteString("failed to ")
|
||||
b.WriteString(auditAttributes.Verb.StringValue())
|
||||
b.WriteString(auditAttributes.Action.StringValue())
|
||||
}
|
||||
|
||||
// Resource: " kind (id)" or " kind".
|
||||
b.WriteString(" ")
|
||||
b.WriteString(resourceAttributes.Resource.Kind().String())
|
||||
b.WriteString(resourceAttributes.ResourceKind.String())
|
||||
if resourceAttributes.ResourceID != "" {
|
||||
b.WriteString(" (")
|
||||
b.WriteString(resourceAttributes.ResourceID)
|
||||
b.WriteString(")")
|
||||
}
|
||||
|
||||
// Target (attach events): " to kind (id)" or " to kind".
|
||||
if resourceAttributes.TargetResource != nil {
|
||||
b.WriteString(" to ")
|
||||
b.WriteString(resourceAttributes.TargetResource.Kind().String())
|
||||
if resourceAttributes.TargetResourceID != "" {
|
||||
b.WriteString(" (")
|
||||
b.WriteString(resourceAttributes.TargetResourceID)
|
||||
b.WriteString(")")
|
||||
}
|
||||
}
|
||||
|
||||
// Error suffix (failure only): ": type (code)" or ": type" or ": (code)" or omitted.
|
||||
if auditAttributes.Outcome == OutcomeFailure {
|
||||
errorType := errorAttributes.ErrorType
|
||||
|
||||
204
pkg/types/audittypes/attributes_test.go
Normal file
204
pkg/types/audittypes/attributes_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package audittypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewAuditAttributesFromHTTP_OutcomeBoundary(t *testing.T) {
|
||||
claims := authtypes.Claims{IdentNProvider: authtypes.IdentNProviderTokenizer}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
expectedOutcome Outcome
|
||||
}{
|
||||
{
|
||||
name: "200_Success",
|
||||
statusCode: 200,
|
||||
expectedOutcome: OutcomeSuccess,
|
||||
},
|
||||
{
|
||||
name: "399_Success",
|
||||
statusCode: 399,
|
||||
expectedOutcome: OutcomeSuccess,
|
||||
},
|
||||
{
|
||||
name: "400_Failure",
|
||||
statusCode: 400,
|
||||
expectedOutcome: OutcomeFailure,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
attrs := NewAuditAttributesFromHTTP(testCase.statusCode, coretypes.VerbUpdate, ActionCategoryConfigurationChange, claims)
|
||||
assert.Equal(t, testCase.expectedOutcome, attrs.Outcome)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBody(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
auditAttributes AuditAttributes
|
||||
principalAttributes PrincipalAttributes
|
||||
resourceAttributes ResourceAttributes
|
||||
errorAttributes ErrorAttributes
|
||||
expectedBody string
|
||||
}{
|
||||
{
|
||||
name: "Success_EmptyResourceID",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbDelete,
|
||||
ActionCategory: ActionCategoryConfigurationChange,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{
|
||||
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
|
||||
PrincipalEmail: valuer.MustNewEmail("test@acme.com"),
|
||||
},
|
||||
resourceAttributes: ResourceAttributes{
|
||||
ResourceID: "",
|
||||
ResourceKind: coretypes.MustNewKind("dashboard"),
|
||||
},
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) deleted dashboard",
|
||||
},
|
||||
{
|
||||
name: "Success_EmptyPrincipalEmail",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbDelete,
|
||||
ActionCategory: ActionCategoryConfigurationChange,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{
|
||||
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
|
||||
PrincipalEmail: valuer.Email{},
|
||||
},
|
||||
resourceAttributes: ResourceAttributes{
|
||||
ResourceID: "abd",
|
||||
ResourceKind: coretypes.MustNewKind("dashboard"),
|
||||
},
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "019a1234-abcd-7000-8000-567800000001 deleted dashboard (abd)",
|
||||
},
|
||||
{
|
||||
name: "Success_EmptyPrincipalIDandEmail",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbDelete,
|
||||
ActionCategory: ActionCategoryConfigurationChange,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{
|
||||
PrincipalID: valuer.UUID{},
|
||||
PrincipalEmail: valuer.Email{},
|
||||
},
|
||||
resourceAttributes: ResourceAttributes{
|
||||
ResourceID: "abd",
|
||||
ResourceKind: coretypes.MustNewKind("dashboard"),
|
||||
},
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "deleted dashboard (abd)",
|
||||
},
|
||||
{
|
||||
name: "Success_AllPresent",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbCreate,
|
||||
ActionCategory: ActionCategoryConfigurationChange,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{
|
||||
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
|
||||
PrincipalEmail: valuer.MustNewEmail("alice@acme.com"),
|
||||
},
|
||||
resourceAttributes: ResourceAttributes{
|
||||
ResourceID: "019b-5678",
|
||||
ResourceKind: coretypes.MustNewKind("dashboard"),
|
||||
},
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "alice@acme.com (019a1234-abcd-7000-8000-567800000001) created dashboard (019b-5678)",
|
||||
},
|
||||
{
|
||||
name: "Success_EmptyEverythingOptional",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbUpdate,
|
||||
ActionCategory: ActionCategoryConfigurationChange,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{},
|
||||
resourceAttributes: ResourceAttributes{
|
||||
ResourceKind: coretypes.MustNewKind("alert-rule"),
|
||||
},
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "updated alert-rule",
|
||||
},
|
||||
{
|
||||
name: "Failure_AllPresent",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbUpdate,
|
||||
ActionCategory: ActionCategoryConfigurationChange,
|
||||
Outcome: OutcomeFailure,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{
|
||||
PrincipalID: valuer.MustNewUUID("019aaaaa-bbbb-7000-8000-cccc00000002"),
|
||||
PrincipalEmail: valuer.MustNewEmail("viewer@acme.com"),
|
||||
},
|
||||
resourceAttributes: ResourceAttributes{
|
||||
ResourceID: "019b-5678",
|
||||
ResourceKind: coretypes.MustNewKind("dashboard"),
|
||||
},
|
||||
errorAttributes: ErrorAttributes{
|
||||
ErrorType: "forbidden",
|
||||
ErrorCode: "authz_forbidden",
|
||||
},
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678): forbidden (authz_forbidden)",
|
||||
},
|
||||
{
|
||||
name: "Failure_ErrorTypeOnly",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbDelete,
|
||||
Outcome: OutcomeFailure,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{
|
||||
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
|
||||
PrincipalEmail: valuer.MustNewEmail("test@acme.com"),
|
||||
},
|
||||
resourceAttributes: ResourceAttributes{
|
||||
ResourceKind: coretypes.MustNewKind("user"),
|
||||
},
|
||||
errorAttributes: ErrorAttributes{
|
||||
ErrorType: "not-found",
|
||||
},
|
||||
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) failed to delete user: not-found",
|
||||
},
|
||||
{
|
||||
name: "Failure_NoErrorDetails",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbCreate,
|
||||
Outcome: OutcomeFailure,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{
|
||||
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
|
||||
PrincipalEmail: valuer.MustNewEmail("test@acme.com"),
|
||||
},
|
||||
resourceAttributes: ResourceAttributes{
|
||||
ResourceID: "019b-5678",
|
||||
ResourceKind: coretypes.MustNewKind("dashboard"),
|
||||
},
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) failed to create dashboard (019b-5678)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
body := newBody(testCase.auditAttributes, testCase.principalAttributes, testCase.resourceAttributes, testCase.errorAttributes)
|
||||
assert.Equal(t, testCase.expectedBody, body)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ type AuditEvent struct {
|
||||
// OTel LogRecord Intrinsic
|
||||
EventName EventName
|
||||
|
||||
// Custom Audit Attributes - Verb
|
||||
// Custom Audit Attributes - Action
|
||||
AuditAttributes AuditAttributes
|
||||
|
||||
// Custom Audit Attributes - Principal
|
||||
@@ -50,15 +50,17 @@ func NewAuditEventFromHTTPRequest(
|
||||
statusCode int,
|
||||
traceID oteltrace.TraceID,
|
||||
spanID oteltrace.SpanID,
|
||||
verb coretypes.Verb,
|
||||
action coretypes.Verb,
|
||||
actionCategory ActionCategory,
|
||||
claims authtypes.Claims,
|
||||
resourceAttributes ResourceAttributes,
|
||||
resourceID string,
|
||||
resourceKind coretypes.Kind,
|
||||
errorType string,
|
||||
errorCode string,
|
||||
) AuditEvent {
|
||||
auditAttributes := NewAuditAttributesFromHTTP(statusCode, verb, actionCategory, claims)
|
||||
auditAttributes := NewAuditAttributesFromHTTP(statusCode, action, actionCategory, claims)
|
||||
principalAttributes := NewPrincipalAttributesFromClaims(claims)
|
||||
resourceAttributes := NewResourceAttributes(resourceID, resourceKind)
|
||||
errorAttributes := NewErrorAttributes(errorType, errorCode)
|
||||
transportAttributes := NewTransportAttributesFromHTTP(req, route, statusCode)
|
||||
|
||||
@@ -67,7 +69,7 @@ func NewAuditEventFromHTTPRequest(
|
||||
TraceID: traceID,
|
||||
SpanID: spanID,
|
||||
Body: newBody(auditAttributes, principalAttributes, resourceAttributes, errorAttributes),
|
||||
EventName: NewEventName(resourceAttributes.Resource.Kind(), auditAttributes.Verb),
|
||||
EventName: NewEventName(resourceAttributes.ResourceKind, auditAttributes.Action),
|
||||
AuditAttributes: auditAttributes,
|
||||
PrincipalAttributes: principalAttributes,
|
||||
ResourceAttributes: resourceAttributes,
|
||||
@@ -87,7 +89,7 @@ func NewPLogsFromAuditEvents(events []AuditEvent, name string, version string, s
|
||||
groups := make(map[resourceKey][]int)
|
||||
order := make([]resourceKey, 0)
|
||||
for i, event := range events {
|
||||
key := resourceKey{kind: event.ResourceAttributes.Resource.Kind().String(), id: event.ResourceAttributes.ResourceID}
|
||||
key := resourceKey{kind: event.ResourceAttributes.ResourceKind.String(), id: event.ResourceAttributes.ResourceID}
|
||||
if _, exists := groups[key]; !exists {
|
||||
order = append(order, key)
|
||||
}
|
||||
@@ -99,8 +101,7 @@ func NewPLogsFromAuditEvents(events []AuditEvent, name string, version string, s
|
||||
resourceAttrs := resourceLogs.Resource().Attributes()
|
||||
resourceAttrs.PutStr(string(semconv.ServiceNameKey), name)
|
||||
resourceAttrs.PutStr(string(semconv.ServiceVersionKey), version)
|
||||
head := events[groups[key][0]]
|
||||
head.ResourceAttributes.PutResource(resourceAttrs, head.PrincipalAttributes.PrincipalOrgID)
|
||||
events[groups[key][0]].ResourceAttributes.PutResource(resourceAttrs)
|
||||
|
||||
scopeLogs := resourceLogs.ScopeLogs().AppendEmpty()
|
||||
scopeLogs.Scope().SetName(scope)
|
||||
|
||||
242
pkg/types/audittypes/event_test.go
Normal file
242
pkg/types/audittypes/event_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package audittypes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
testDashboardKind = coretypes.MustNewKind("dashboard")
|
||||
)
|
||||
|
||||
func TestNewAuditEventFromHTTPRequest(t *testing.T) {
|
||||
traceID := oteltrace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
spanID := oteltrace.SpanID{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
route string
|
||||
statusCode int
|
||||
action coretypes.Verb
|
||||
category ActionCategory
|
||||
claims authtypes.Claims
|
||||
resourceID string
|
||||
resourceKind coretypes.Kind
|
||||
errorType string
|
||||
errorCode string
|
||||
expectedOutcome Outcome
|
||||
expectedBody string
|
||||
}{
|
||||
{
|
||||
name: "Success_DashboardCreated",
|
||||
method: http.MethodPost,
|
||||
path: "/api/v1/dashboards",
|
||||
route: "/api/v1/dashboards",
|
||||
statusCode: http.StatusOK,
|
||||
action: coretypes.VerbCreate,
|
||||
category: ActionCategoryConfigurationChange,
|
||||
claims: authtypes.Claims{UserID: "019a1234-abcd-7000-8000-567800000001", Email: "alice@acme.com", OrgID: "019a-0000-0000-0001", IdentNProvider: authtypes.IdentNProviderTokenizer},
|
||||
resourceID: "019b-5678-efgh-9012",
|
||||
resourceKind: testDashboardKind,
|
||||
expectedOutcome: OutcomeSuccess,
|
||||
expectedBody: "alice@acme.com (019a1234-abcd-7000-8000-567800000001) created dashboard (019b-5678-efgh-9012)",
|
||||
},
|
||||
{
|
||||
name: "Failure_ForbiddenDashboardUpdate",
|
||||
method: http.MethodPut,
|
||||
path: "/api/v1/dashboards/019b-5678-efgh-9012",
|
||||
route: "/api/v1/dashboards/{id}",
|
||||
statusCode: http.StatusForbidden,
|
||||
action: coretypes.VerbUpdate,
|
||||
category: ActionCategoryConfigurationChange,
|
||||
claims: authtypes.Claims{UserID: "019aaaaa-bbbb-7000-8000-cccc00000002", Email: "viewer@acme.com", OrgID: "019a-0000-0000-0001", IdentNProvider: authtypes.IdentNProviderTokenizer},
|
||||
resourceID: "019b-5678-efgh-9012",
|
||||
resourceKind: testDashboardKind,
|
||||
errorType: "forbidden",
|
||||
errorCode: "authz_forbidden",
|
||||
expectedOutcome: OutcomeFailure,
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678-efgh-9012): forbidden (authz_forbidden)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(testCase.method, testCase.path, nil)
|
||||
|
||||
event := NewAuditEventFromHTTPRequest(
|
||||
req,
|
||||
testCase.route,
|
||||
testCase.statusCode,
|
||||
traceID,
|
||||
spanID,
|
||||
testCase.action,
|
||||
testCase.category,
|
||||
testCase.claims,
|
||||
testCase.resourceID,
|
||||
testCase.resourceKind,
|
||||
testCase.errorType,
|
||||
testCase.errorCode,
|
||||
)
|
||||
|
||||
assert.Equal(t, testCase.expectedOutcome, event.AuditAttributes.Outcome)
|
||||
assert.Equal(t, testCase.expectedBody, event.Body)
|
||||
assert.Equal(t, testCase.resourceKind, event.ResourceAttributes.ResourceKind)
|
||||
assert.Equal(t, testCase.resourceID, event.ResourceAttributes.ResourceID)
|
||||
assert.Equal(t, testCase.action, event.AuditAttributes.Action)
|
||||
assert.Equal(t, testCase.category, event.AuditAttributes.ActionCategory)
|
||||
assert.Equal(t, testCase.route, event.TransportAttributes.HTTPRoute)
|
||||
assert.Equal(t, testCase.statusCode, event.TransportAttributes.HTTPStatusCode)
|
||||
assert.Equal(t, testCase.method, event.TransportAttributes.HTTPMethod)
|
||||
assert.Equal(t, traceID, event.TraceID)
|
||||
assert.Equal(t, spanID, event.SpanID)
|
||||
assert.Equal(t, testCase.errorType, event.ErrorAttributes.ErrorType)
|
||||
assert.Equal(t, testCase.errorCode, event.ErrorAttributes.ErrorCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newTestEvent(resourceKind coretypes.Kind, resourceID string, action coretypes.Verb) AuditEvent {
|
||||
return AuditEvent{
|
||||
Body: resourceKind.String() + "." + action.PastTense(),
|
||||
EventName: NewEventName(resourceKind, action),
|
||||
AuditAttributes: AuditAttributes{
|
||||
Action: action,
|
||||
ActionCategory: ActionCategoryConfigurationChange,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
ResourceAttributes: ResourceAttributes{
|
||||
ResourceKind: resourceKind,
|
||||
ResourceID: resourceID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPLogsFromAuditEvents(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
events []AuditEvent
|
||||
expectedResourceLogs int
|
||||
expectedResourceKinds []string
|
||||
expectedResourceIDs []string
|
||||
expectedLogRecordCounts []int
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
events: []AuditEvent{},
|
||||
expectedResourceLogs: 0,
|
||||
},
|
||||
{
|
||||
name: "SingleEvent",
|
||||
events: []AuditEvent{
|
||||
newTestEvent(testDashboardKind, "d-001", coretypes.VerbCreate),
|
||||
},
|
||||
expectedResourceLogs: 1,
|
||||
expectedResourceKinds: []string{"dashboard"},
|
||||
expectedResourceIDs: []string{"d-001"},
|
||||
expectedLogRecordCounts: []int{1},
|
||||
},
|
||||
{
|
||||
name: "SameResource_MultipleEvents",
|
||||
events: []AuditEvent{
|
||||
newTestEvent(testDashboardKind, "d-001", coretypes.VerbCreate),
|
||||
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
|
||||
newTestEvent(testDashboardKind, "d-001", coretypes.VerbDelete),
|
||||
},
|
||||
expectedResourceLogs: 1,
|
||||
expectedResourceKinds: []string{"dashboard"},
|
||||
expectedResourceIDs: []string{"d-001"},
|
||||
expectedLogRecordCounts: []int{3},
|
||||
},
|
||||
{
|
||||
name: "DifferentResources_SeparateGroups",
|
||||
events: []AuditEvent{
|
||||
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
|
||||
newTestEvent(coretypes.MustNewKind("user"), "u-001", coretypes.VerbDelete),
|
||||
},
|
||||
expectedResourceLogs: 2,
|
||||
expectedResourceKinds: []string{"dashboard", "user"},
|
||||
expectedResourceIDs: []string{"d-001", "u-001"},
|
||||
expectedLogRecordCounts: []int{1, 1},
|
||||
},
|
||||
{
|
||||
name: "SameKind_DifferentIDs_SeparateGroups",
|
||||
events: []AuditEvent{
|
||||
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
|
||||
newTestEvent(testDashboardKind, "d-002", coretypes.VerbDelete),
|
||||
},
|
||||
expectedResourceLogs: 2,
|
||||
expectedResourceKinds: []string{"dashboard", "dashboard"},
|
||||
expectedResourceIDs: []string{"d-001", "d-002"},
|
||||
expectedLogRecordCounts: []int{1, 1},
|
||||
},
|
||||
{
|
||||
name: "InterleavedResources_GroupedCorrectly",
|
||||
events: []AuditEvent{
|
||||
newTestEvent(testDashboardKind, "d-001", coretypes.VerbCreate),
|
||||
newTestEvent(coretypes.MustNewKind("user"), "u-001", coretypes.VerbUpdate),
|
||||
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
|
||||
newTestEvent(coretypes.MustNewKind("user"), "u-001", coretypes.VerbDelete),
|
||||
newTestEvent(testDashboardKind, "d-001", coretypes.VerbDelete),
|
||||
},
|
||||
expectedResourceLogs: 2,
|
||||
expectedResourceKinds: []string{"dashboard", "user"},
|
||||
expectedResourceIDs: []string{"d-001", "u-001"},
|
||||
expectedLogRecordCounts: []int{3, 2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
logs := NewPLogsFromAuditEvents(testCase.events, "signoz", "0.90.0", "signoz.audit")
|
||||
|
||||
assert.Equal(t, testCase.expectedResourceLogs, logs.ResourceLogs().Len())
|
||||
|
||||
for i := 0; i < logs.ResourceLogs().Len(); i++ {
|
||||
resourceLogs := logs.ResourceLogs().At(i)
|
||||
resourceAttrs := resourceLogs.Resource().Attributes()
|
||||
|
||||
// Verify service resource attributes
|
||||
serviceName, exists := resourceAttrs.Get("service.name")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "signoz", serviceName.Str())
|
||||
|
||||
serviceVersion, exists := resourceAttrs.Get("service.version")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "0.90.0", serviceVersion.Str())
|
||||
|
||||
// Verify audit resource attributes on Resource (not event attributes)
|
||||
kind, exists := resourceAttrs.Get("signoz.audit.resource.kind")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, testCase.expectedResourceKinds[i], kind.Str())
|
||||
|
||||
id, exists := resourceAttrs.Get("signoz.audit.resource.id")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, testCase.expectedResourceIDs[i], id.Str())
|
||||
|
||||
// Verify scope
|
||||
assert.Equal(t, 1, resourceLogs.ScopeLogs().Len())
|
||||
assert.Equal(t, "signoz.audit", resourceLogs.ScopeLogs().At(0).Scope().Name())
|
||||
|
||||
// Verify log record count per group
|
||||
assert.Equal(t, testCase.expectedLogRecordCounts[i], resourceLogs.ScopeLogs().At(0).LogRecords().Len())
|
||||
|
||||
// Verify resource attrs are NOT in log record event attributes
|
||||
for j := 0; j < resourceLogs.ScopeLogs().At(0).LogRecords().Len(); j++ {
|
||||
recordAttrs := resourceLogs.ScopeLogs().At(0).LogRecords().At(j).Attributes()
|
||||
_, hasKind := recordAttrs.Get("signoz.audit.resource.kind")
|
||||
_, hasID := recordAttrs.Get("signoz.audit.resource.id")
|
||||
assert.False(t, hasKind, "resource.kind must not be in log record attributes")
|
||||
assert.False(t, hasID, "resource.id must not be in log record attributes")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -38,12 +38,6 @@ var Kinds = []Kind{
|
||||
KindMeterMetrics,
|
||||
KindLogsField,
|
||||
KindTracesField,
|
||||
KindLLMPricingRule,
|
||||
KindSpanMapperGroup,
|
||||
KindSpanMapper,
|
||||
KindZeusProfile,
|
||||
KindZeusHost,
|
||||
KindMetricField,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -84,10 +78,4 @@ var (
|
||||
KindMeterMetrics = MustNewKind("meter-metrics")
|
||||
KindLogsField = MustNewKind("logs-field")
|
||||
KindTracesField = MustNewKind("traces-field")
|
||||
KindLLMPricingRule = MustNewKind("llm-pricing-rule")
|
||||
KindSpanMapperGroup = MustNewKind("span-mapper-group")
|
||||
KindSpanMapper = MustNewKind("span-mapper")
|
||||
KindZeusProfile = MustNewKind("zeus-profile")
|
||||
KindZeusHost = MustNewKind("zeus-host")
|
||||
KindMetricField = MustNewKind("metric-field")
|
||||
)
|
||||
|
||||
@@ -69,18 +69,6 @@ var Resources = []Resource{
|
||||
ResourceMetaResourcesLogsField,
|
||||
ResourceMetaResourceTracesField,
|
||||
ResourceMetaResourcesTracesField,
|
||||
ResourceMetaResourceLLMPricingRule,
|
||||
ResourceMetaResourcesLLMPricingRule,
|
||||
ResourceMetaResourceSpanMapperGroup,
|
||||
ResourceMetaResourcesSpanMapperGroup,
|
||||
ResourceMetaResourceSpanMapper,
|
||||
ResourceMetaResourcesSpanMapper,
|
||||
ResourceMetaResourceZeusProfile,
|
||||
ResourceMetaResourcesZeusProfile,
|
||||
ResourceMetaResourceZeusHost,
|
||||
ResourceMetaResourcesZeusHost,
|
||||
ResourceMetaResourceMetricField,
|
||||
ResourceMetaResourcesMetricField,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -152,16 +140,4 @@ var (
|
||||
ResourceMetaResourcesLogsField = NewResourceMetaResources(KindLogsField)
|
||||
ResourceMetaResourceTracesField = NewResourceMetaResource(KindTracesField)
|
||||
ResourceMetaResourcesTracesField = NewResourceMetaResources(KindTracesField)
|
||||
ResourceMetaResourceLLMPricingRule = NewResourceMetaResource(KindLLMPricingRule)
|
||||
ResourceMetaResourcesLLMPricingRule = NewResourceMetaResources(KindLLMPricingRule)
|
||||
ResourceMetaResourceSpanMapperGroup = NewResourceMetaResource(KindSpanMapperGroup)
|
||||
ResourceMetaResourcesSpanMapperGroup = NewResourceMetaResources(KindSpanMapperGroup)
|
||||
ResourceMetaResourceSpanMapper = NewResourceMetaResource(KindSpanMapper)
|
||||
ResourceMetaResourcesSpanMapper = NewResourceMetaResources(KindSpanMapper)
|
||||
ResourceMetaResourceZeusProfile = NewResourceMetaResource(KindZeusProfile)
|
||||
ResourceMetaResourcesZeusProfile = NewResourceMetaResources(KindZeusProfile)
|
||||
ResourceMetaResourceZeusHost = NewResourceMetaResource(KindZeusHost)
|
||||
ResourceMetaResourcesZeusHost = NewResourceMetaResources(KindZeusHost)
|
||||
ResourceMetaResourceMetricField = NewResourceMetaResource(KindMetricField)
|
||||
ResourceMetaResourcesMetricField = NewResourceMetaResources(KindMetricField)
|
||||
)
|
||||
|
||||
105
pkg/types/inframonitoringtypes/clusters.go
Normal file
105
pkg/types/inframonitoringtypes/clusters.go
Normal file
@@ -0,0 +1,105 @@
|
||||
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()
|
||||
}
|
||||
15
pkg/types/inframonitoringtypes/clusters_constants.go
Normal file
15
pkg/types/inframonitoringtypes/clusters_constants.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const (
|
||||
ClustersOrderByCPU = "cpu"
|
||||
ClustersOrderByCPUAllocatable = "cpu_allocatable"
|
||||
ClustersOrderByMemory = "memory"
|
||||
ClustersOrderByMemoryAllocatable = "memory_allocatable"
|
||||
)
|
||||
|
||||
var ClustersValidOrderByKeys = []string{
|
||||
ClustersOrderByCPU,
|
||||
ClustersOrderByCPUAllocatable,
|
||||
ClustersOrderByMemory,
|
||||
ClustersOrderByMemoryAllocatable,
|
||||
}
|
||||
291
pkg/types/inframonitoringtypes/clusters_test.go
Normal file
291
pkg/types/inframonitoringtypes/clusters_test.go
Normal file
@@ -0,0 +1,291 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
99
pkg/types/inframonitoringtypes/namespaces.go
Normal file
99
pkg/types/inframonitoringtypes/namespaces.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
type Namespaces struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []NamespaceRecord `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 NamespaceRecord struct {
|
||||
NamespaceName string `json:"namespaceName" required:"true"`
|
||||
NamespaceCPU float64 `json:"namespaceCPU" required:"true"`
|
||||
NamespaceMemory float64 `json:"namespaceMemory" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableNamespaces is the request body for the v2 namespaces list API.
|
||||
type PostableNamespaces 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 PostableNamespaces contains acceptable values.
|
||||
func (req *PostableNamespaces) 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(NamespacesValidOrderByKeys, 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 *PostableNamespaces) UnmarshalJSON(data []byte) error {
|
||||
type raw PostableNamespaces
|
||||
var decoded raw
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
*req = PostableNamespaces(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
11
pkg/types/inframonitoringtypes/namespaces_constants.go
Normal file
11
pkg/types/inframonitoringtypes/namespaces_constants.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const (
|
||||
NamespacesOrderByCPU = "cpu"
|
||||
NamespacesOrderByMemory = "memory"
|
||||
)
|
||||
|
||||
var NamespacesValidOrderByKeys = []string{
|
||||
NamespacesOrderByCPU,
|
||||
NamespacesOrderByMemory,
|
||||
}
|
||||
237
pkg/types/inframonitoringtypes/namespaces_test.go
Normal file
237
pkg/types/inframonitoringtypes/namespaces_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
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 TestPostableNamespaces_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *PostableNamespaces
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid request",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil request",
|
||||
req: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time zero",
|
||||
req: &PostableNamespaces{
|
||||
Start: 0,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time negative",
|
||||
req: &PostableNamespaces{
|
||||
Start: -1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "end time zero",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 0,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time greater than end time",
|
||||
req: &PostableNamespaces{
|
||||
Start: 2000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time equal to end time",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit zero",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 0,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit negative",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: -10,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit exceeds max",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 5001,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "offset negative",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: -5,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy nil is valid",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key cpu and direction asc",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: NamespacesOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key memory and direction desc",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: NamespacesOrderByMemory,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with pod_phase key is rejected",
|
||||
req: &PostableNamespaces{
|
||||
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: &PostableNamespaces{
|
||||
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: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: NamespacesOrderByMemory,
|
||||
},
|
||||
},
|
||||
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,7 +245,6 @@ 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
|
||||
|
||||
@@ -13,7 +13,6 @@ pytest_plugins = [
|
||||
"fixtures.zookeeper",
|
||||
"fixtures.signoz",
|
||||
"fixtures.audit",
|
||||
"fixtures.auditor",
|
||||
"fixtures.logs",
|
||||
"fixtures.traces",
|
||||
"fixtures.metrics",
|
||||
|
||||
183
tests/fixtures/auditor.py
vendored
183
tests/fixtures/auditor.py
vendored
@@ -1,183 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import reuse, types
|
||||
from fixtures.auth import find_user_by_email
|
||||
|
||||
# Filename used for the audit log inside the host-mounted tmp dir. The same
|
||||
# absolute path is mounted into the SigNoz container at the same location, so
|
||||
# the file is visible to both the backend (writer) and the test runner (reader).
|
||||
AUDIT_FILE_NAME = "audit.log"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _AuditDir:
|
||||
"""Cacheable wrapper around the audit directory path so the reuse.wrap
|
||||
machinery can persist it across pytest runs alongside the SigNoz container."""
|
||||
|
||||
path: str
|
||||
|
||||
def __cache__(self) -> dict:
|
||||
return {"path": self.path}
|
||||
|
||||
def __log__(self) -> str:
|
||||
return f"AuditDir(path={self.path})"
|
||||
|
||||
|
||||
@pytest.fixture(name="audit_dir", scope="package")
|
||||
def audit_dir(
|
||||
tmpfs: Callable[[str], types.LegacyPath],
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> str:
|
||||
"""Host tmp directory mounted into the SigNoz container as the auditor file path.
|
||||
|
||||
Mirrors the sqlite/clickhouse pattern: a tmpfs directory is created on the
|
||||
host and bind-mounted into the container at the same absolute path, so the
|
||||
audit log file is reachable from both sides without docker exec. The path
|
||||
is cached via reuse.wrap so re-runs under --reuse keep using the same dir
|
||||
that the long-lived SigNoz container has bind-mounted.
|
||||
"""
|
||||
|
||||
def create() -> _AuditDir:
|
||||
return _AuditDir(path=str(tmpfs("auditor")))
|
||||
|
||||
def delete(_: _AuditDir) -> None:
|
||||
pass
|
||||
|
||||
def restore(cache: dict) -> _AuditDir:
|
||||
return _AuditDir(path=cache["path"])
|
||||
|
||||
return reuse.wrap(
|
||||
request,
|
||||
pytestconfig,
|
||||
"auditor_dir",
|
||||
lambda: _AuditDir(path=""),
|
||||
create,
|
||||
delete,
|
||||
restore,
|
||||
).path
|
||||
|
||||
|
||||
@pytest.fixture(name="audit_file_path", scope="package")
|
||||
def audit_file_path(audit_dir: str) -> str: # pylint: disable=redefined-outer-name
|
||||
return os.path.join(audit_dir, AUDIT_FILE_NAME)
|
||||
|
||||
|
||||
def ensure_user_active(
|
||||
signoz: types.SigNoz,
|
||||
admin_token: str,
|
||||
email: str,
|
||||
role: str,
|
||||
password: str,
|
||||
name: str = "",
|
||||
) -> str:
|
||||
"""Invite + activate a user, or return the existing user's id if already present.
|
||||
|
||||
Idempotent counterpart to fixtures.auth.create_active_user — needed because the
|
||||
auditor suite reuses a long-lived SigNoz container across pytest runs and would
|
||||
otherwise hit a 409 on the second invite.
|
||||
"""
|
||||
invite = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": email, "role": role, "name": name},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
if invite.status_code == HTTPStatus.CONFLICT:
|
||||
return find_user_by_email(signoz, admin_token, email)["id"]
|
||||
assert invite.status_code == HTTPStatus.CREATED, invite.text
|
||||
|
||||
invited_user = invite.json()["data"]
|
||||
activate = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": password, "token": invited_user["token"]},
|
||||
timeout=5,
|
||||
)
|
||||
assert activate.status_code == HTTPStatus.NO_CONTENT, activate.text
|
||||
return invited_user["id"]
|
||||
|
||||
|
||||
def read_audit_records(audit_file_path: str) -> list[dict[str, Any]]: # pylint: disable=redefined-outer-name
|
||||
"""Read every audit log record from the host-side audit file.
|
||||
|
||||
Each line of the file is one OTLP-Logs JSON object containing all events
|
||||
flushed in a single export batch. Returns the flattened list of LogRecord
|
||||
dicts across every line, with the parent resource attributes merged into
|
||||
each record's attributes so signoz.audit.resource.kind and
|
||||
signoz.audit.resource.id are reachable via attr_value.
|
||||
"""
|
||||
if not os.path.exists(audit_file_path):
|
||||
return []
|
||||
|
||||
records: list[dict[str, Any]] = []
|
||||
with open(audit_file_path, encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
# Partial line caught mid-write between flush syscalls; the
|
||||
# next poll will re-read the full file once the write
|
||||
# completes. Skip without failing the test.
|
||||
continue
|
||||
for resource_log in payload.get("resourceLogs", []):
|
||||
resource_attrs = resource_log.get("resource", {}).get("attributes", [])
|
||||
for scope_log in resource_log.get("scopeLogs", []):
|
||||
for record in scope_log.get("logRecords", []):
|
||||
merged = dict(record)
|
||||
merged["attributes"] = list(record.get("attributes", [])) + list(resource_attrs)
|
||||
records.append(merged)
|
||||
return records
|
||||
|
||||
|
||||
def attr_value(record: dict[str, Any], key: str) -> Any:
|
||||
"""Return the value of an OTLP-JSON attribute by key, or None if absent."""
|
||||
for kv in record.get("attributes", []):
|
||||
if kv.get("key") != key:
|
||||
continue
|
||||
value = kv.get("value", {})
|
||||
for kind in ("stringValue", "intValue", "boolValue", "doubleValue"):
|
||||
if kind in value:
|
||||
return value[kind]
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def find_event(records: list[dict[str, Any]], event_name: str, **filters: Any) -> dict[str, Any] | None:
|
||||
"""Find the first record whose eventName matches and whose audit attributes match every filter."""
|
||||
for record in records:
|
||||
if record.get("eventName") != event_name:
|
||||
continue
|
||||
if all(attr_value(record, k) == v for k, v in filters.items()):
|
||||
return record
|
||||
return None
|
||||
|
||||
|
||||
def wait_for_event(
|
||||
audit_file_path: str, # pylint: disable=redefined-outer-name
|
||||
event_name: str,
|
||||
timeout: float = 2.0,
|
||||
interval: float = 0.1,
|
||||
**filters: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""Poll the audit file until an event matching event_name + filters appears."""
|
||||
deadline = time.monotonic() + timeout
|
||||
last_records: list[dict[str, Any]] = []
|
||||
while time.monotonic() < deadline:
|
||||
last_records = read_audit_records(audit_file_path)
|
||||
event = find_event(last_records, event_name, **filters)
|
||||
if event is not None:
|
||||
return event
|
||||
time.sleep(interval)
|
||||
raise AssertionError(f"audit event {event_name!r} matching {filters} not found within {timeout}s (saw {len(last_records)} records: {[r.get('eventName') for r in last_records]})")
|
||||
3
tests/fixtures/querier.py
vendored
3
tests/fixtures/querier.py
vendored
@@ -72,6 +72,7 @@ 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] = {
|
||||
@@ -83,6 +84,8 @@ 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,6 +73,30 @@ 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(
|
||||
|
||||
13
tests/fixtures/signoz.py
vendored
13
tests/fixtures/signoz.py
vendored
@@ -16,7 +16,7 @@ from fixtures.logger import setup_logger
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def create_signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
def create_signoz(
|
||||
network: Network,
|
||||
zeus: types.TestContainerDocker,
|
||||
gateway: types.TestContainerDocker,
|
||||
@@ -26,15 +26,10 @@ def create_signoz( # pylint: disable=too-many-arguments,too-many-positional-arg
|
||||
pytestconfig: pytest.Config,
|
||||
cache_key: str = "signoz",
|
||||
env_overrides: dict | None = None,
|
||||
volume_mappings: list[tuple[str, str]] | None = None,
|
||||
) -> types.SigNoz:
|
||||
"""
|
||||
Factory function for creating a SigNoz container.
|
||||
|
||||
Accepts optional env_overrides to customize the container environment, and
|
||||
optional volume_mappings (host_path, container_path) tuples to mount host
|
||||
directories into the container — mirrors how sqlite/clickhouse fixtures
|
||||
expose tmp paths back to the test runner.
|
||||
Accepts optional env_overrides to customize the container environment.
|
||||
"""
|
||||
|
||||
def create() -> types.SigNoz:
|
||||
@@ -109,10 +104,6 @@ def create_signoz( # pylint: disable=too-many-arguments,too-many-positional-arg
|
||||
"rw",
|
||||
)
|
||||
|
||||
if volume_mappings:
|
||||
for host_path, container_path in volume_mappings:
|
||||
container.with_volume_mapping(host_path, container_path, "rw")
|
||||
|
||||
container.start()
|
||||
|
||||
def ready(container: DockerContainer) -> None:
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auditor import attr_value, ensure_user_active, wait_for_event
|
||||
from fixtures.auth import (
|
||||
USER_ADMIN_EMAIL,
|
||||
USER_ADMIN_PASSWORD,
|
||||
USER_VIEWER_EMAIL,
|
||||
USER_VIEWER_NAME,
|
||||
USER_VIEWER_PASSWORD,
|
||||
find_user_by_email,
|
||||
)
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def test_session_deleted_event_appears_in_file(
|
||||
signoz: types.SigNoz,
|
||||
apply_license: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
audit_file_path: str,
|
||||
) -> None:
|
||||
"""An admin logout posts to DELETE /api/v2/sessions; the audit middleware
|
||||
captures the post-auth claims and the file provider writes session.deleted."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/sessions"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
record = wait_for_event(
|
||||
audit_file_path,
|
||||
"session.deleted",
|
||||
**{
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.principal.email": USER_ADMIN_EMAIL,
|
||||
},
|
||||
)
|
||||
assert attr_value(record, "signoz.audit.resource.kind") == "session"
|
||||
assert attr_value(record, "signoz.audit.action_category") == "access_control"
|
||||
assert record["severityText"] == "INFO"
|
||||
|
||||
|
||||
def test_audit_records_failure_outcome(
|
||||
signoz: types.SigNoz,
|
||||
apply_license: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
audit_file_path: str,
|
||||
) -> None:
|
||||
"""A viewer hitting an admin-only mutation must produce an audit record with
|
||||
outcome=failure and the captured error.type. Proves the middleware writes
|
||||
on the 4xx path, not just the happy path."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
ensure_user_active(
|
||||
signoz,
|
||||
admin_token,
|
||||
USER_VIEWER_EMAIL,
|
||||
"VIEWER",
|
||||
USER_VIEWER_PASSWORD,
|
||||
USER_VIEWER_NAME,
|
||||
)
|
||||
|
||||
admin_user = find_user_by_email(signoz, admin_token, USER_ADMIN_EMAIL)
|
||||
viewer_token = get_token(USER_VIEWER_EMAIL, USER_VIEWER_PASSWORD)
|
||||
|
||||
forbidden = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{admin_user['id']}"),
|
||||
json={"displayName": "should not work"},
|
||||
headers={"Authorization": f"Bearer {viewer_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert forbidden.status_code == HTTPStatus.FORBIDDEN, forbidden.text
|
||||
|
||||
record = wait_for_event(
|
||||
audit_file_path,
|
||||
"user.updated",
|
||||
**{
|
||||
"signoz.audit.outcome": "failure",
|
||||
"signoz.audit.principal.email": USER_VIEWER_EMAIL,
|
||||
"signoz.audit.resource.id": admin_user["id"],
|
||||
},
|
||||
)
|
||||
assert record["severityText"] == "ERROR"
|
||||
assert attr_value(record, "signoz.audit.error.type") is not None
|
||||
@@ -1,122 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auditor import attr_value, ensure_user_active, wait_for_event
|
||||
from fixtures.auth import (
|
||||
USER_ADMIN_EMAIL,
|
||||
USER_ADMIN_PASSWORD,
|
||||
USER_EDITOR_EMAIL,
|
||||
USER_EDITOR_NAME,
|
||||
USER_EDITOR_PASSWORD,
|
||||
change_user_role,
|
||||
find_user_by_email,
|
||||
)
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def test_user_updated_event_appears_in_file(
|
||||
signoz: types.SigNoz,
|
||||
apply_license: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
audit_file_path: str,
|
||||
) -> None:
|
||||
"""Admin renames an editor user via PUT /api/v2/users/{id}; the file provider
|
||||
writes user.updated with the editor id as the resource."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
editor_id = ensure_user_active(
|
||||
signoz,
|
||||
admin_token,
|
||||
USER_EDITOR_EMAIL,
|
||||
"EDITOR",
|
||||
USER_EDITOR_PASSWORD,
|
||||
USER_EDITOR_NAME,
|
||||
)
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{editor_id}"),
|
||||
json={"displayName": "Renamed Editor"},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
record = wait_for_event(
|
||||
audit_file_path,
|
||||
"user.updated",
|
||||
**{
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.resource.id": editor_id,
|
||||
"signoz.audit.principal.email": USER_ADMIN_EMAIL,
|
||||
},
|
||||
)
|
||||
assert attr_value(record, "signoz.audit.action_category") == "configuration_change"
|
||||
assert attr_value(record, "signoz.audit.resource.kind") == "user"
|
||||
|
||||
|
||||
def test_user_role_change_emits_created_and_deleted_events(
|
||||
signoz: types.SigNoz,
|
||||
apply_license: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
audit_file_path: str,
|
||||
) -> None:
|
||||
"""Toggling the editor user's managed role between signoz-editor and signoz-viewer
|
||||
fires both DELETE and POST against /api/v2/users/{id}/roles; the file provider
|
||||
writes one user-role.deleted and one user-role.created tied to the editor id.
|
||||
|
||||
The toggle direction is computed from the current role so the test is idempotent
|
||||
across re-runs of the long-lived auditor SigNoz container.
|
||||
"""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
editor = find_user_by_email(signoz, admin_token, USER_EDITOR_EMAIL)
|
||||
editor_id = editor["id"]
|
||||
|
||||
roles_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{editor_id}/roles"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert roles_response.status_code == HTTPStatus.OK, roles_response.text
|
||||
managed_role = next(
|
||||
(r for r in roles_response.json()["data"] if r["name"] in ("signoz-editor", "signoz-viewer")),
|
||||
None,
|
||||
)
|
||||
assert managed_role is not None, "editor user is missing both managed roles"
|
||||
|
||||
if managed_role["name"] == "signoz-editor":
|
||||
old_role, new_role = "signoz-editor", "signoz-viewer"
|
||||
else:
|
||||
old_role, new_role = "signoz-viewer", "signoz-editor"
|
||||
|
||||
change_user_role(signoz, admin_token, editor_id, old_role, new_role)
|
||||
|
||||
deleted = wait_for_event(
|
||||
audit_file_path,
|
||||
"user-role.deleted",
|
||||
**{
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.resource.id": editor_id,
|
||||
"signoz.audit.principal.email": USER_ADMIN_EMAIL,
|
||||
},
|
||||
)
|
||||
assert attr_value(deleted, "signoz.audit.action_category") == "access_control"
|
||||
assert attr_value(deleted, "signoz.audit.resource.kind") == "user-role"
|
||||
|
||||
created = wait_for_event(
|
||||
audit_file_path,
|
||||
"user-role.created",
|
||||
**{
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.resource.id": editor_id,
|
||||
"signoz.audit.principal.email": USER_ADMIN_EMAIL,
|
||||
},
|
||||
)
|
||||
assert attr_value(created, "signoz.audit.action_category") == "access_control"
|
||||
assert attr_value(created, "signoz.audit.resource.kind") == "user-role"
|
||||
@@ -1,44 +0,0 @@
|
||||
import pytest
|
||||
from testcontainers.core.container import Network
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import create_signoz
|
||||
|
||||
|
||||
@pytest.fixture(name="signoz", scope="package")
|
||||
def signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
network: Network,
|
||||
zeus: types.TestContainerDocker,
|
||||
gateway: types.TestContainerDocker,
|
||||
sqlstore: types.TestContainerSQL,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
audit_dir: str,
|
||||
audit_file_path: str,
|
||||
) -> types.SigNoz:
|
||||
"""Package-scoped SigNoz container configured with the file auditor.
|
||||
|
||||
BatchSize is set to 1 so every audited request flushes to disk on the moreC
|
||||
path without waiting on the periodic ticker. FlushInterval stays short so
|
||||
the periodic flush has bounded lag if BatchSize is ever raised. The audit
|
||||
directory is bind-mounted from the host (see fixtures.auditor.audit_dir)
|
||||
so tests can read the file with a plain open() call.
|
||||
"""
|
||||
return create_signoz(
|
||||
network=network,
|
||||
zeus=zeus,
|
||||
gateway=gateway,
|
||||
sqlstore=sqlstore,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="signoz_auditor",
|
||||
env_overrides={
|
||||
"SIGNOZ_AUDITOR_PROVIDER": "file",
|
||||
"SIGNOZ_AUDITOR_FILE_PATH": audit_file_path,
|
||||
"SIGNOZ_AUDITOR_BATCH__SIZE": "1",
|
||||
"SIGNOZ_AUDITOR_FLUSH__INTERVAL": "100ms",
|
||||
},
|
||||
volume_mappings=[(audit_dir, audit_dir)],
|
||||
)
|
||||
@@ -37,7 +37,7 @@ def test_audit_list_all(
|
||||
"signoz.audit.principal.id": "user-010",
|
||||
"signoz.audit.principal.email": "ops@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action": "create",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) created alert-rule (alert-001)",
|
||||
@@ -55,7 +55,7 @@ def test_audit_list_all(
|
||||
"signoz.audit.principal.id": "user-010",
|
||||
"signoz.audit.principal.email": "ops@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) updated saved-view (view-001)",
|
||||
@@ -73,7 +73,7 @@ def test_audit_list_all(
|
||||
"signoz.audit.principal.id": "user-010",
|
||||
"signoz.audit.principal.email": "ops@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
@@ -146,7 +146,7 @@ def test_audit_list_all(
|
||||
id="filter_by_principal_type",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.resource.kind = 'dashboard' AND signoz.audit.verb = 'delete'",
|
||||
"signoz.audit.resource.kind = 'dashboard' AND signoz.audit.action = 'delete'",
|
||||
1,
|
||||
{"dashboard.deleted"},
|
||||
id="filter_by_resource_kind_and_action",
|
||||
@@ -177,7 +177,7 @@ def test_audit_filter(
|
||||
"signoz.audit.principal.id": "user-001",
|
||||
"signoz.audit.principal.email": "alice@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action": "create",
|
||||
"signoz.audit.action_category": "configuration_change",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
@@ -195,7 +195,7 @@ def test_audit_filter(
|
||||
"signoz.audit.principal.id": "user-001",
|
||||
"signoz.audit.principal.email": "alice@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.audit.action_category": "configuration_change",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
@@ -213,7 +213,7 @@ def test_audit_filter(
|
||||
"signoz.audit.principal.id": "user-002",
|
||||
"signoz.audit.principal.email": "viewer@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.action": "delete",
|
||||
"signoz.audit.action_category": "configuration_change",
|
||||
"signoz.audit.outcome": "failure",
|
||||
"signoz.audit.error.type": "forbidden",
|
||||
@@ -234,7 +234,7 @@ def test_audit_filter(
|
||||
"signoz.audit.principal.id": "sa-001",
|
||||
"signoz.audit.principal.email": "",
|
||||
"signoz.audit.principal.type": "service_account",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action": "create",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
@@ -252,7 +252,7 @@ def test_audit_filter(
|
||||
"signoz.audit.principal.id": "user-001",
|
||||
"signoz.audit.principal.email": "alice@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "login",
|
||||
"signoz.audit.action": "login",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
@@ -310,7 +310,7 @@ def test_audit_scalar_count_failures(
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-050",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.action": "delete",
|
||||
"signoz.audit.outcome": "failure",
|
||||
},
|
||||
body="user-050 failed to delete dashboard",
|
||||
@@ -327,7 +327,7 @@ def test_audit_scalar_count_failures(
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-060",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.audit.outcome": "failure",
|
||||
},
|
||||
body="user-060 failed to update alert-rule",
|
||||
@@ -344,7 +344,7 @@ def test_audit_scalar_count_failures(
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-050",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="user-050 updated dashboard",
|
||||
@@ -400,7 +400,7 @@ def test_audit_does_not_leak_into_logs(
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-admin",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="user-admin updated organization (org-999)",
|
||||
@@ -430,5 +430,5 @@ def test_audit_does_not_leak_into_logs(
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0].get("rows") or []
|
||||
|
||||
audit_bodies = [row["data"]["body"] for row in rows if "signoz.audit" in row["data"].get("attributes_string", {}).get("signoz.audit.verb", "")]
|
||||
audit_bodies = [row["data"]["body"] for row in rows if "signoz.audit" in row["data"].get("attributes_string", {}).get("signoz.audit.action", "")]
|
||||
assert len(audit_bodies) == 0
|
||||
|
||||
487
tests/integration/tests/querier/15_trace_operator.py
Normal file
487
tests/integration/tests/querier/15_trace_operator.py
Normal file
@@ -0,0 +1,487 @@
|
||||
"""
|
||||
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.
|
||||
3. Expression operators (=>, ->, &&, ||, A NOT B) with and without returnSpansFrom.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class _SpanDef:
|
||||
"""Span spec relative to 'now'. parent_idx=-1 for root spans."""
|
||||
|
||||
name: str
|
||||
service: str
|
||||
op_type: str
|
||||
duration_s: int
|
||||
time_offset_s: int
|
||||
parent_idx: int = -1
|
||||
extra_attrs: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
def _build_trace(now: datetime, trace_id: str, spans: list[_SpanDef]) -> list[Traces]:
|
||||
span_ids = [TraceIdGenerator.span_id() for _ in spans]
|
||||
result = []
|
||||
for i, (defn, span_id) in enumerate(zip(spans, span_ids)):
|
||||
parent_id = "" if defn.parent_idx < 0 else span_ids[defn.parent_idx]
|
||||
kind = TracesKind.SPAN_KIND_SERVER if defn.parent_idx < 0 else TracesKind.SPAN_KIND_INTERNAL
|
||||
result.append(
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=defn.time_offset_s),
|
||||
duration=timedelta(seconds=defn.duration_s),
|
||||
trace_id=trace_id,
|
||||
span_id=span_id,
|
||||
parent_span_id=parent_id,
|
||||
name=defn.name,
|
||||
kind=kind,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": defn.service},
|
||||
attributes={"operation.type": defn.op_type, **defn.extra_attrs},
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Order-by variants
|
||||
# ---------------------------------------------------------------------------
|
||||
# Each case uses a unique op_type prefix so spans inserted by earlier
|
||||
# parametrize runs (shared DB session) are never picked up by later ones.
|
||||
|
||||
|
||||
@dataclass
|
||||
class _OrderByCase:
|
||||
id: str
|
||||
trace1_spans: list[_SpanDef]
|
||||
trace2_spans: list[_SpanDef]
|
||||
filter_a: str
|
||||
filter_b: str
|
||||
expression: str
|
||||
select_fields: list[TelemetryFieldKey] | None
|
||||
order: list[OrderBy]
|
||||
expected_rows: list[dict] # ordered; each dict is a partial match against row data
|
||||
|
||||
|
||||
_ORDER_BY_CASES: list[_OrderByCase] = [
|
||||
# Order by attribute absent from selectFields — NOT_FOUND_COLUMN_IN_BLOCK regression guard.
|
||||
_OrderByCase(
|
||||
id="field_not_in_select",
|
||||
trace1_spans=[
|
||||
_SpanDef("fnis-gp", "svc-a", "fnis-grandparent", 5, 10, extra_attrs={"http.method": "POST"}),
|
||||
_SpanDef("fnis-mid", "svc-a", "fnis-middle", 3, 9, parent_idx=0),
|
||||
_SpanDef("fnis-gc", "svc-a", "fnis-grandchild", 1, 8, parent_idx=1),
|
||||
],
|
||||
trace2_spans=[
|
||||
_SpanDef("fnis-gp", "svc-b", "fnis-grandparent", 5, 7, extra_attrs={"http.method": "GET"}),
|
||||
_SpanDef("fnis-mid", "svc-b", "fnis-middle", 3, 6, parent_idx=0),
|
||||
_SpanDef("fnis-gc", "svc-b", "fnis-grandchild", 1, 5, parent_idx=1),
|
||||
],
|
||||
filter_a="operation.type = 'fnis-grandparent'",
|
||||
filter_b="operation.type = 'fnis-grandchild'",
|
||||
expression="A -> B",
|
||||
select_fields=[TelemetryFieldKey(name="service.name", field_data_type="string", field_context="resource")],
|
||||
order=[OrderBy(
|
||||
key=TelemetryFieldKey(name="http.method", field_data_type="string", field_context="attribute"),
|
||||
direction="desc",
|
||||
)],
|
||||
# POST > GET in DESC → svc-a first
|
||||
expected_rows=[{"service.name": "svc-a"}, {"service.name": "svc-b"}],
|
||||
),
|
||||
# Order by a core span field (duration_nano) with no explicit selectFields.
|
||||
_OrderByCase(
|
||||
id="core_span_field",
|
||||
trace1_spans=[
|
||||
_SpanDef("csf-parent-long", "svc-long", "csf-parent", 5, 10),
|
||||
_SpanDef("csf-child-long", "svc-long", "csf-child", 1, 9, parent_idx=0),
|
||||
],
|
||||
trace2_spans=[
|
||||
_SpanDef("csf-parent-short", "svc-short", "csf-parent", 1, 8),
|
||||
_SpanDef("csf-child-short", "svc-short", "csf-child", 1, 7, parent_idx=0),
|
||||
],
|
||||
filter_a="operation.type = 'csf-parent'",
|
||||
filter_b="operation.type = 'csf-child'",
|
||||
expression="A => B",
|
||||
select_fields=None,
|
||||
order=[OrderBy(key=TelemetryFieldKey(name="duration_nano", field_context="span"), direction="desc")],
|
||||
# 5 s parent first, 1 s parent second
|
||||
expected_rows=[{"name": "csf-parent-long"}, {"name": "csf-parent-short"}],
|
||||
),
|
||||
# Order by a non-core attribute that IS in selectFields — checks ordering and field presence.
|
||||
_OrderByCase(
|
||||
id="non_core_field_in_select",
|
||||
trace1_spans=[
|
||||
_SpanDef("ncis-parent-post", "svc-post", "ncis-parent", 3, 10, extra_attrs={"http.method": "POST"}),
|
||||
_SpanDef("ncis-child-post", "svc-post", "ncis-child", 1, 9, parent_idx=0),
|
||||
],
|
||||
trace2_spans=[
|
||||
_SpanDef("ncis-parent-get", "svc-get", "ncis-parent", 3, 8, extra_attrs={"http.method": "GET"}),
|
||||
_SpanDef("ncis-child-get", "svc-get", "ncis-child", 1, 7, parent_idx=0),
|
||||
],
|
||||
filter_a="operation.type = 'ncis-parent'",
|
||||
filter_b="operation.type = 'ncis-child'",
|
||||
expression="A => B",
|
||||
select_fields=[TelemetryFieldKey(name="http.method", field_data_type="string", field_context="attribute")],
|
||||
order=[OrderBy(
|
||||
key=TelemetryFieldKey(name="http.method", field_data_type="string", field_context="attribute"),
|
||||
direction="desc",
|
||||
)],
|
||||
# POST > GET in DESC; http.method must appear in both rows (it is in selectFields)
|
||||
expected_rows=[{"http.method": "POST"}, {"http.method": "GET"}],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", [pytest.param(c, id=c.id) for c in _ORDER_BY_CASES])
|
||||
def test_trace_operator_query_order_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
case: _OrderByCase,
|
||||
) -> None:
|
||||
"""
|
||||
Verifies that trace operator queries honour the order-by clause.
|
||||
|
||||
Cases:
|
||||
- field_not_in_select: order by attribute absent from selectFields
|
||||
(NOT_FOUND_COLUMN_IN_BLOCK regression guard).
|
||||
- core_span_field: order by duration_nano with no explicit selectFields.
|
||||
- non_core_field_in_select: order by attribute present in selectFields.
|
||||
"""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
|
||||
insert_traces(
|
||||
_build_trace(now, trace_id_1, case.trace1_spans)
|
||||
+ _build_trace(now, trace_id_2, case.trace2_spans)
|
||||
+ _build_trace(now, TraceIdGenerator.trace_id(), [_SpanDef("noise-span", "svc-noise", "noise-op", 1, 2)])
|
||||
)
|
||||
|
||||
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", case.filter_a),
|
||||
_builder_query("B", case.filter_b),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression=case.expression,
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
select_fields=case.select_fields,
|
||||
order=case.order,
|
||||
).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) == len(case.expected_rows)
|
||||
|
||||
for i, (row, expected) in enumerate(zip(rows, case.expected_rows)):
|
||||
for key, value in expected.items():
|
||||
assert row["data"].get(key) == value, (
|
||||
f"[{case.id}] row {i}: expected {key}={value!r}, got {row['data'].get(key)!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Operator × returnSpansFrom matrix
|
||||
# ---------------------------------------------------------------------------
|
||||
# Each case uses a unique op_type prefix so DB rows from earlier parametrize
|
||||
# runs never contaminate later ones (the session-level ClickHouse is shared).
|
||||
#
|
||||
# Operators tested: => -> && || (A NOT B)
|
||||
# For each operator two cases:
|
||||
# "default" — returnSpansFrom="" → result comes from the expression's root CTE
|
||||
# "return_A" — returnSpansFrom="A" → result comes from the A sub-query CTE
|
||||
#
|
||||
# Root-CTE semantics:
|
||||
# => A spans that have a DIRECT child matching B
|
||||
# -> A spans that are ancestors of any B span
|
||||
# && A spans from traces that also contain a B span
|
||||
# || UNION of A spans and B spans
|
||||
# A NOT B A spans from traces that contain no B span
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ExprCase:
|
||||
id: str
|
||||
traces: list[list[_SpanDef]] # one inner list per trace
|
||||
filter_a: str
|
||||
filter_b: str
|
||||
expression: str
|
||||
return_spans_from: str # "" → use expression root CTE
|
||||
expected_names: set[str] # span.name values that must appear (exact set)
|
||||
|
||||
|
||||
_EXPR_CASES: list[_ExprCase] = [
|
||||
# ── A => B (direct child) ────────────────────────────────────────────────
|
||||
# "default": root CTE = A spans that have a direct B child
|
||||
_ExprCase(
|
||||
id="direct_child_default",
|
||||
traces=[
|
||||
[
|
||||
_SpanDef("dcd-root", "svc-dcd-a", "dcd-root", 5, 10),
|
||||
_SpanDef("dcd-leaf", "svc-dcd-a", "dcd-leaf", 2, 9, parent_idx=0),
|
||||
],
|
||||
[_SpanDef("dcd-root-only", "svc-dcd-b", "dcd-root", 2, 7)], # A but no B child
|
||||
],
|
||||
filter_a="operation.type = 'dcd-root'",
|
||||
filter_b="operation.type = 'dcd-leaf'",
|
||||
expression="A => B",
|
||||
return_spans_from="",
|
||||
expected_names={"dcd-root"}, # only the root that HAS a direct child
|
||||
),
|
||||
# "return_A": returns ALL A spans, bypassing the expression filter
|
||||
_ExprCase(
|
||||
id="direct_child_return_A",
|
||||
traces=[
|
||||
[
|
||||
_SpanDef("dca-root", "svc-dca-a", "dca-root", 5, 10),
|
||||
_SpanDef("dca-leaf", "svc-dca-a", "dca-leaf", 2, 9, parent_idx=0),
|
||||
],
|
||||
[_SpanDef("dca-root-only", "svc-dca-b", "dca-root", 2, 7)],
|
||||
],
|
||||
filter_a="operation.type = 'dca-root'",
|
||||
filter_b="operation.type = 'dca-leaf'",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
expected_names={"dca-root", "dca-root-only"},
|
||||
),
|
||||
# ── A -> B (indirect descendant) ─────────────────────────────────────────
|
||||
_ExprCase(
|
||||
id="indirect_descendant_default",
|
||||
traces=[
|
||||
[
|
||||
_SpanDef("idd-gp", "svc-idd-a", "idd-gp", 5, 10),
|
||||
_SpanDef("idd-mid", "svc-idd-a", "idd-mid", 3, 9, parent_idx=0),
|
||||
_SpanDef("idd-gc", "svc-idd-a", "idd-gc", 1, 8, parent_idx=1),
|
||||
],
|
||||
[_SpanDef("idd-gp-only", "svc-idd-b", "idd-gp", 2, 7)], # A but no B descendant
|
||||
],
|
||||
filter_a="operation.type = 'idd-gp'",
|
||||
filter_b="operation.type = 'idd-gc'",
|
||||
expression="A -> B",
|
||||
return_spans_from="",
|
||||
expected_names={"idd-gp"},
|
||||
),
|
||||
_ExprCase(
|
||||
id="indirect_descendant_return_A",
|
||||
traces=[
|
||||
[
|
||||
_SpanDef("ida-gp", "svc-ida-a", "ida-gp", 5, 10),
|
||||
_SpanDef("ida-mid", "svc-ida-a", "ida-mid", 3, 9, parent_idx=0),
|
||||
_SpanDef("ida-gc", "svc-ida-a", "ida-gc", 1, 8, parent_idx=1),
|
||||
],
|
||||
[_SpanDef("ida-gp-only", "svc-ida-b", "ida-gp", 2, 7)],
|
||||
],
|
||||
filter_a="operation.type = 'ida-gp'",
|
||||
filter_b="operation.type = 'ida-gc'",
|
||||
expression="A -> B",
|
||||
return_spans_from="A",
|
||||
expected_names={"ida-gp", "ida-gp-only"},
|
||||
),
|
||||
# ── A && B ────────────────────────────────────────────────────────────────
|
||||
_ExprCase(
|
||||
id="and_default",
|
||||
traces=[
|
||||
[
|
||||
_SpanDef("and-root", "svc-and-a", "and-root", 5, 10),
|
||||
_SpanDef("and-leaf", "svc-and-a", "and-leaf", 2, 9, parent_idx=0),
|
||||
],
|
||||
[_SpanDef("and-root-only", "svc-and-b", "and-root", 2, 7)], # A but no B in trace
|
||||
],
|
||||
filter_a="operation.type = 'and-root'",
|
||||
filter_b="operation.type = 'and-leaf'",
|
||||
expression="A && B",
|
||||
return_spans_from="",
|
||||
expected_names={"and-root"}, # A from traces that also contain B
|
||||
),
|
||||
_ExprCase(
|
||||
id="and_return_A",
|
||||
traces=[
|
||||
[
|
||||
_SpanDef("ana-root", "svc-ana-a", "ana-root", 5, 10),
|
||||
_SpanDef("ana-leaf", "svc-ana-a", "ana-leaf", 2, 9, parent_idx=0),
|
||||
],
|
||||
[_SpanDef("ana-root-only", "svc-ana-b", "ana-root", 2, 7)],
|
||||
],
|
||||
filter_a="operation.type = 'ana-root'",
|
||||
filter_b="operation.type = 'ana-leaf'",
|
||||
expression="A && B",
|
||||
return_spans_from="A",
|
||||
expected_names={"ana-root", "ana-root-only"},
|
||||
),
|
||||
# ── A || B ────────────────────────────────────────────────────────────────
|
||||
_ExprCase(
|
||||
id="or_default",
|
||||
traces=[
|
||||
[_SpanDef("ord-a-span", "svc-ord-a", "ord-a", 5, 10)],
|
||||
[_SpanDef("ord-b-span", "svc-ord-b", "ord-b", 2, 7)],
|
||||
],
|
||||
filter_a="operation.type = 'ord-a'",
|
||||
filter_b="operation.type = 'ord-b'",
|
||||
expression="A || B",
|
||||
return_spans_from="",
|
||||
expected_names={"ord-a-span", "ord-b-span"}, # UNION of both A and B
|
||||
),
|
||||
_ExprCase(
|
||||
id="or_return_A",
|
||||
traces=[
|
||||
[_SpanDef("ora-a-span", "svc-ora-a", "ora-a", 5, 10)],
|
||||
[_SpanDef("ora-b-span", "svc-ora-b", "ora-b", 2, 7)],
|
||||
],
|
||||
filter_a="operation.type = 'ora-a'",
|
||||
filter_b="operation.type = 'ora-b'",
|
||||
expression="A || B",
|
||||
return_spans_from="A",
|
||||
expected_names={"ora-a-span"},
|
||||
),
|
||||
# ── A NOT B (binary not) ──────────────────────────────────────────────────
|
||||
# Unary NOT A is skipped: its root CTE reads from all_spans (unbounded by
|
||||
# filter), making row counts non-deterministic across a shared test session.
|
||||
_ExprCase(
|
||||
id="not_binary_default",
|
||||
traces=[
|
||||
[
|
||||
_SpanDef("nbd-root-with-child", "svc-nbd-a", "nbd-root", 5, 10),
|
||||
_SpanDef("nbd-child", "svc-nbd-a", "nbd-child", 2, 9, parent_idx=0),
|
||||
],
|
||||
[_SpanDef("nbd-root-no-child", "svc-nbd-b", "nbd-root", 2, 7)], # A, no B
|
||||
],
|
||||
filter_a="operation.type = 'nbd-root'",
|
||||
filter_b="operation.type = 'nbd-child'",
|
||||
expression="A NOT B",
|
||||
return_spans_from="",
|
||||
expected_names={"nbd-root-no-child"}, # A from traces that have no B
|
||||
),
|
||||
_ExprCase(
|
||||
id="not_binary_return_A",
|
||||
traces=[
|
||||
[
|
||||
_SpanDef("nba-root-with-child", "svc-nba-a", "nba-root", 5, 10),
|
||||
_SpanDef("nba-child", "svc-nba-a", "nba-child", 2, 9, parent_idx=0),
|
||||
],
|
||||
[_SpanDef("nba-root-no-child", "svc-nba-b", "nba-root", 2, 7)],
|
||||
],
|
||||
filter_a="operation.type = 'nba-root'",
|
||||
filter_b="operation.type = 'nba-child'",
|
||||
expression="A NOT B",
|
||||
return_spans_from="A",
|
||||
expected_names={"nba-root-with-child", "nba-root-no-child"}, # ALL A spans
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", [pytest.param(c, id=c.id) for c in _EXPR_CASES])
|
||||
def test_trace_operator_expressions(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
case: _ExprCase,
|
||||
) -> None:
|
||||
"""
|
||||
Matrix of expression operators × returnSpansFrom settings.
|
||||
|
||||
For each operator (=>, ->, &&, ||, A NOT B) two cases verify:
|
||||
- default (returnSpansFrom=""): result comes from the expression's root CTE,
|
||||
so only spans satisfying the full structural predicate are returned.
|
||||
- return_A (returnSpansFrom="A"): result comes from the raw A sub-query CTE,
|
||||
bypassing the structural filter, returning ALL spans matching filter A.
|
||||
"""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
all_spans: list[Traces] = []
|
||||
for span_defs in case.traces:
|
||||
all_spans.extend(_build_trace(now, TraceIdGenerator.trace_id(), span_defs))
|
||||
# Noise: op_type "noise-op" matches no filter in any case; surfacing it would
|
||||
# mean a filter regression, which the set-equality assertion below would catch.
|
||||
all_spans.extend(_build_trace(now, TraceIdGenerator.trace_id(), [_SpanDef("noise-span", "svc-noise", "noise-op", 1, 2)]))
|
||||
insert_traces(all_spans)
|
||||
|
||||
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", case.filter_a),
|
||||
_builder_query("B", case.filter_b),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression=case.expression,
|
||||
return_spans_from=case.return_spans_from,
|
||||
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 []
|
||||
|
||||
actual_names = {row["data"]["name"] for row in rows}
|
||||
assert actual_names == case.expected_names, (
|
||||
f"[{case.id}] expected spans {case.expected_names}, got {actual_names}"
|
||||
)
|
||||
@@ -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 replaces existing role, verify via GET."""
|
||||
"""POST /{id}/roles adds a role alongside existing ones."""
|
||||
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 (replaces viewer)
|
||||
# assign editor role (additive — viewer stays)
|
||||
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 only editor role is present (viewer was replaced)
|
||||
# verify both viewer and editor roles are present
|
||||
roles_resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -68,9 +68,31 @@ 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) == 1
|
||||
assert len(role_names) == 2
|
||||
assert "signoz-viewer" in role_names
|
||||
assert "signoz-editor" in role_names
|
||||
assert "signoz-viewer" not 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
|
||||
|
||||
|
||||
def test_assign_role_idempotent(
|
||||
@@ -103,16 +125,16 @@ def test_assign_role_idempotent(
|
||||
assert role_names.count("signoz-viewer") == 1
|
||||
|
||||
|
||||
def test_assign_role_replaces_access(
|
||||
def test_assign_role_expands_access(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""After role replacement, SA loses old permissions and gains new ones."""
|
||||
"""Adding a higher-privilege role expands the SA's access."""
|
||||
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-replace-access", role="signoz-viewer")
|
||||
service_account_id, api_key = create_service_account_with_key(signoz, token, "sa-role-expand-access", role="signoz-viewer")
|
||||
|
||||
# viewer should get 403 on admin-only endpoint
|
||||
resp = requests.get(
|
||||
@@ -122,7 +144,7 @@ def test_assign_role_replaces_access(
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.FORBIDDEN, f"Expected 403 for viewer on admin endpoint, got {resp.status_code}: {resp.text}"
|
||||
|
||||
# assign admin role (replaces viewer)
|
||||
# assign admin role (additive — viewer stays)
|
||||
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"),
|
||||
@@ -138,9 +160,9 @@ def test_assign_role_replaces_access(
|
||||
headers={"SIGNOZ-API-KEY": api_key},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, f"Expected 200 for admin on admin endpoint, got {resp.status_code}: {resp.text}"
|
||||
assert resp.status_code == HTTPStatus.OK, f"Expected 200 after adding admin role, got {resp.status_code}: {resp.text}"
|
||||
|
||||
# verify only admin role is present
|
||||
# verify both roles are present
|
||||
roles_resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -148,9 +170,9 @@ def test_assign_role_replaces_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) == 1
|
||||
assert len(role_names) == 2
|
||||
assert "signoz-admin" in role_names
|
||||
assert "signoz-viewer" not in role_names
|
||||
assert "signoz-viewer" in role_names
|
||||
|
||||
|
||||
def test_remove_role_from_service_account(
|
||||
@@ -158,13 +180,22 @@ 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 a role."""
|
||||
"""DELETE /{id}/roles/{rid} revokes one role while keeping others."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
service_account_id = create_service_account(signoz, token, "sa-remove-role", role="signoz-editor")
|
||||
|
||||
editor_role_id = find_role_by_name(signoz, token, "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 the role
|
||||
# remove editor role
|
||||
editor_role_id = find_role_by_name(signoz, token, "signoz-editor")
|
||||
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}"},
|
||||
@@ -172,7 +203,7 @@ def test_remove_role_from_service_account(
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.NO_CONTENT, resp.text
|
||||
|
||||
# verify role is gone
|
||||
# verify editor is gone but admin remains
|
||||
roles_resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -181,6 +212,7 @@ 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