mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-22 20:00:29 +01:00
Compare commits
98 Commits
json-featu
...
infraM/v2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d81cec4c29 | ||
|
|
49744c6104 | ||
|
|
2147627baf | ||
|
|
824f92a88f | ||
|
|
983d4fe4f2 | ||
|
|
833af794c3 | ||
|
|
21b51d1fcc | ||
|
|
56f22682c8 | ||
|
|
9c8359940c | ||
|
|
4050880275 | ||
|
|
5e775f64f2 | ||
|
|
0189f23f46 | ||
|
|
49a36d4e3d | ||
|
|
9407d658ab | ||
|
|
5035712485 | ||
|
|
bab17c3615 | ||
|
|
37b44f4db9 | ||
|
|
99dd6e5f1e | ||
|
|
9c7131fa6a | ||
|
|
ad889a2e1d | ||
|
|
a4f6d0cbf5 | ||
|
|
589bed7c16 | ||
|
|
93843a1f48 | ||
|
|
88c43108fc | ||
|
|
ed4cf540e8 | ||
|
|
9e2dfa9033 | ||
|
|
d98d5d68ee | ||
|
|
2cb1c3b73b | ||
|
|
ae7ca497ad | ||
|
|
a579916961 | ||
|
|
4a16d56abf | ||
|
|
642b5ac3f0 | ||
|
|
a12112619c | ||
|
|
014785f1bc | ||
|
|
58ee797b10 | ||
|
|
82d236742f | ||
|
|
397e1ad5be | ||
|
|
8d6b25ca9b | ||
|
|
5fa6bd8b8d | ||
|
|
bd9977483b | ||
|
|
50fbdfeeef | ||
|
|
e2b1b73e87 | ||
|
|
cb9f3fd3e5 | ||
|
|
232acc343d | ||
|
|
2025afdccc | ||
|
|
d2f4d4af93 | ||
|
|
47ff7bbb8e | ||
|
|
724071c5dc | ||
|
|
4d24979358 | ||
|
|
042943b10a | ||
|
|
48a9be7ec8 | ||
|
|
a9504b2120 | ||
|
|
8755887c4a | ||
|
|
4cb4662b3a | ||
|
|
e6900dabc8 | ||
|
|
c1ba389b63 | ||
|
|
3a1f40234f | ||
|
|
2e4891fa63 | ||
|
|
04ebc0bec7 | ||
|
|
271f9b81ed | ||
|
|
6fa815c294 | ||
|
|
63ec518efb | ||
|
|
c4ca20dd90 | ||
|
|
e56cc4222b | ||
|
|
07d2944d7c | ||
|
|
dea01ae36a | ||
|
|
62ea5b54e2 | ||
|
|
e549a7e42f | ||
|
|
90e2ebb11f | ||
|
|
61baa1be7a | ||
|
|
b946fa665f | ||
|
|
2e049556e4 | ||
|
|
492a5e70d7 | ||
|
|
ba1f2771e8 | ||
|
|
7458fb4855 | ||
|
|
5f55f3938b | ||
|
|
3e8102485c | ||
|
|
861c682ea5 | ||
|
|
c8e5895dff | ||
|
|
82d72e7edb | ||
|
|
a3f8ecaaf1 | ||
|
|
19aada656c | ||
|
|
b21bb4280f | ||
|
|
bc0a4fdb5c | ||
|
|
37fb0e9254 | ||
|
|
aecfa1a174 | ||
|
|
b869d23d94 | ||
|
|
6ee3d44f76 | ||
|
|
462e554107 | ||
|
|
66afa73e6f | ||
|
|
54c604bcf4 | ||
|
|
c1be02ba54 | ||
|
|
d3c7ba8f45 | ||
|
|
039c4a0496 | ||
|
|
51a94b6bbc | ||
|
|
bbfbb94f52 | ||
|
|
d1eb9ef16f | ||
|
|
3db00f8bc3 |
@@ -2287,6 +2287,205 @@ components:
|
||||
enabled:
|
||||
type: boolean
|
||||
type: object
|
||||
InframonitoringtypesHostFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostStatus'
|
||||
type: object
|
||||
InframonitoringtypesHostRecord:
|
||||
properties:
|
||||
activeHostCount:
|
||||
type: integer
|
||||
cpu:
|
||||
format: double
|
||||
type: number
|
||||
diskUsage:
|
||||
format: double
|
||||
type: number
|
||||
hostName:
|
||||
type: string
|
||||
inactiveHostCount:
|
||||
type: integer
|
||||
load15:
|
||||
format: double
|
||||
type: number
|
||||
memory:
|
||||
format: double
|
||||
type: number
|
||||
meta:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
status:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostStatus'
|
||||
wait:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- hostName
|
||||
- status
|
||||
- activeHostCount
|
||||
- inactiveHostCount
|
||||
- cpu
|
||||
- memory
|
||||
- wait
|
||||
- load15
|
||||
- diskUsage
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesHostStatus:
|
||||
enum:
|
||||
- active
|
||||
- inactive
|
||||
- ""
|
||||
type: string
|
||||
InframonitoringtypesHosts:
|
||||
properties:
|
||||
endTimeBeforeRetention:
|
||||
type: boolean
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostRecord'
|
||||
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
|
||||
InframonitoringtypesPodPhase:
|
||||
enum:
|
||||
- pending
|
||||
- running
|
||||
- succeeded
|
||||
- failed
|
||||
- ""
|
||||
type: string
|
||||
InframonitoringtypesPodRecord:
|
||||
properties:
|
||||
meta:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
podAge:
|
||||
format: int64
|
||||
type: integer
|
||||
podCPU:
|
||||
format: double
|
||||
type: number
|
||||
podCPULimit:
|
||||
format: double
|
||||
type: number
|
||||
podCPURequest:
|
||||
format: double
|
||||
type: number
|
||||
podMemory:
|
||||
format: double
|
||||
type: number
|
||||
podMemoryLimit:
|
||||
format: double
|
||||
type: number
|
||||
podMemoryRequest:
|
||||
format: double
|
||||
type: number
|
||||
podPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodPhase'
|
||||
podUID:
|
||||
type: string
|
||||
type: object
|
||||
InframonitoringtypesPods:
|
||||
properties:
|
||||
endTimeBeforeRetention:
|
||||
type: boolean
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
$ref: '#/components/schemas/InframonitoringtypesResponseType'
|
||||
warning:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
|
||||
type: object
|
||||
InframonitoringtypesPostableHosts:
|
||||
properties:
|
||||
end:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostFilter'
|
||||
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
|
||||
InframonitoringtypesPostablePods:
|
||||
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
|
||||
type: object
|
||||
InframonitoringtypesRequiredMetricsCheck:
|
||||
properties:
|
||||
missingMetrics:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- missingMetrics
|
||||
type: object
|
||||
InframonitoringtypesResponseType:
|
||||
enum:
|
||||
- list
|
||||
- grouped_list
|
||||
type: string
|
||||
MetricsexplorertypesInspectMetricsRequest:
|
||||
properties:
|
||||
end:
|
||||
@@ -9853,6 +10052,140 @@ paths:
|
||||
summary: Health check
|
||||
tags:
|
||||
- health
|
||||
/api/v2/infra_monitoring/hosts:
|
||||
post:
|
||||
deprecated: false
|
||||
description: '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.'
|
||||
operationId: ListHosts
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPostableHosts'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHosts'
|
||||
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 Hosts for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/pods:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'Returns a paginated list of Kubernetes pods with key metrics:
|
||||
CPU usage, CPU request/limit utilization, memory working set, memory request/limit
|
||||
utilization, current pod phase (pending/running/succeeded/failed), and pod
|
||||
age (ms since start time). Each pod includes metadata attributes (namespace,
|
||||
node, workload owner such as deployment/statefulset/daemonset/job/cronjob,
|
||||
cluster). Supports filtering via a filter expression, custom groupBy to aggregate
|
||||
pods by any attribute, ordering by any of the seven metrics (cpu, cpu_request,
|
||||
cpu_limit, memory, memory_request, memory_limit, phase), and pagination via
|
||||
offset/limit. The response type is ''list'' for the default k8s.pod.uid 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.'
|
||||
operationId: ListPods
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPostablePods'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPods'
|
||||
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 Pods for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/livez:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
189
frontend/src/api/generated/services/inframonitoring/index.ts
Normal file
189
frontend/src/api/generated/services/inframonitoring/index.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import type {
|
||||
MutationFunction,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
} from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type {
|
||||
InframonitoringtypesPostableHostsDTO,
|
||||
InframonitoringtypesPostablePodsDTO,
|
||||
ListHosts200,
|
||||
ListPods200,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @summary List Hosts for Infra Monitoring
|
||||
*/
|
||||
export const listHosts = (
|
||||
inframonitoringtypesPostableHostsDTO: BodyType<InframonitoringtypesPostableHostsDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListHosts200>({
|
||||
url: `/api/v2/infra_monitoring/hosts`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: inframonitoringtypesPostableHostsDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListHostsMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listHosts>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableHostsDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listHosts>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableHostsDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['listHosts'];
|
||||
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 listHosts>>,
|
||||
{ data: BodyType<InframonitoringtypesPostableHostsDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return listHosts(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ListHostsMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listHosts>>
|
||||
>;
|
||||
export type ListHostsMutationBody = BodyType<InframonitoringtypesPostableHostsDTO>;
|
||||
export type ListHostsMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List Hosts for Infra Monitoring
|
||||
*/
|
||||
export const useListHosts = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listHosts>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableHostsDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof listHosts>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableHostsDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getListHostsMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the seven metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit, phase), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid 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.
|
||||
* @summary List Pods for Infra Monitoring
|
||||
*/
|
||||
export const listPods = (
|
||||
inframonitoringtypesPostablePodsDTO: BodyType<InframonitoringtypesPostablePodsDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListPods200>({
|
||||
url: `/api/v2/infra_monitoring/pods`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: inframonitoringtypesPostablePodsDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListPodsMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listPods>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostablePodsDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listPods>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostablePodsDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['listPods'];
|
||||
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 listPods>>,
|
||||
{ data: BodyType<InframonitoringtypesPostablePodsDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return listPods(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ListPodsMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listPods>>
|
||||
>;
|
||||
export type ListPodsMutationBody = BodyType<InframonitoringtypesPostablePodsDTO>;
|
||||
export type ListPodsMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List Pods for Infra Monitoring
|
||||
*/
|
||||
export const useListPods = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listPods>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostablePodsDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof listPods>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostablePodsDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getListPodsMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
@@ -3051,6 +3051,240 @@ export interface GlobaltypesTokenizerConfigDTO {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesHostFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
filterByStatus?: InframonitoringtypesHostStatusDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesHostRecordDTOMeta = {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
|
||||
export interface InframonitoringtypesHostRecordDTO {
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
activeHostCount: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
cpu: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
diskUsage: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
hostName: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
inactiveHostCount: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
load15: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
memory: number;
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
meta: InframonitoringtypesHostRecordDTOMeta;
|
||||
status: InframonitoringtypesHostStatusDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
wait: number;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesHostStatusDTO {
|
||||
active = 'active',
|
||||
inactive = 'inactive',
|
||||
'' = '',
|
||||
}
|
||||
export interface InframonitoringtypesHostsDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
records: InframonitoringtypesHostRecordDTO[] | null;
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total: number;
|
||||
type: InframonitoringtypesResponseTypeDTO;
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesPodPhaseDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
succeeded = 'succeeded',
|
||||
failed = 'failed',
|
||||
'' = '',
|
||||
}
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesPodRecordDTOMeta = {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
|
||||
export interface InframonitoringtypesPodRecordDTO {
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
meta?: InframonitoringtypesPodRecordDTOMeta;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
podAge?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
podCPU?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
podCPULimit?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
podCPURequest?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
podMemory?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
podMemoryLimit?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
podMemoryRequest?: number;
|
||||
podPhase?: InframonitoringtypesPodPhaseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
podUID?: string;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPodsDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
endTimeBeforeRetention?: boolean;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
records?: InframonitoringtypesPodRecordDTO[] | null;
|
||||
requiredMetricsCheck?: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total?: number;
|
||||
type?: InframonitoringtypesResponseTypeDTO;
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableHostsDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: InframonitoringtypesHostFilterDTO;
|
||||
/**
|
||||
* @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 InframonitoringtypesPostablePodsDTO {
|
||||
/**
|
||||
* @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 InframonitoringtypesRequiredMetricsCheckDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
missingMetrics: string[] | null;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesResponseTypeDTO {
|
||||
list = 'list',
|
||||
grouped_list = 'grouped_list',
|
||||
}
|
||||
export interface MetricsexplorertypesInspectMetricsRequestDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -6636,6 +6870,22 @@ export type Healthz503 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListHosts200 = {
|
||||
data: InframonitoringtypesHostsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListPods200 = {
|
||||
data: InframonitoringtypesPodsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type Livez200 = {
|
||||
data: FactoryResponseDTO;
|
||||
/**
|
||||
|
||||
70
pkg/apiserver/signozapiserver/inframonitoring.go
Normal file
70
pkg/apiserver/signozapiserver/inframonitoring.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/infra_monitoring/hosts", handler.New(
|
||||
provider.authZ.ViewAccess(provider.infraMonitoringHandler.ListHosts),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListHosts",
|
||||
Tags: []string{"inframonitoring"},
|
||||
Summary: "List Hosts for Infra Monitoring",
|
||||
Description: "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.",
|
||||
Request: new(inframonitoringtypes.PostableHosts),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(inframonitoringtypes.Hosts),
|
||||
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/pods", handler.New(
|
||||
provider.authZ.ViewAccess(provider.infraMonitoringHandler.ListPods),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListPods",
|
||||
Tags: []string{"inframonitoring"},
|
||||
Summary: "List Pods for Infra Monitoring",
|
||||
Description: "Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the seven metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit, phase), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid 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.",
|
||||
Request: new(inframonitoringtypes.PostablePods),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(inframonitoringtypes.Pods),
|
||||
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/onboarding", handler.New(
|
||||
provider.authZ.ViewAccess(provider.infraMonitoringHandler.GetOnboarding),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetOnboarding",
|
||||
Tags: []string{"inframonitoring"},
|
||||
Summary: "Get Onboarding Status for Infra Monitoring",
|
||||
Description: "Returns the per-tab readiness of the infra-monitoring section selected by the 'type' query parameter (hosts, processes, pods, nodes, deployments, daemonsets, statefulsets, jobs, namespaces, clusters, volumes). For each collector receiver or processor that contributes required metrics or attributes, lists what is present and what is missing, with a prebuilt user-facing message and a docs link per missing component. Default-enabled metrics are those expected as soon as the receiver is configured; optional metrics require 'enabled: true' in receiver config. 'ready' is true only when every missing list is empty.",
|
||||
RequestQuery: new(inframonitoringtypes.PostableOnboarding),
|
||||
Response: new(inframonitoringtypes.Onboarding),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/promote"
|
||||
@@ -49,6 +50,7 @@ type provider struct {
|
||||
dashboardModule dashboard.Module
|
||||
dashboardHandler dashboard.Handler
|
||||
metricsExplorerHandler metricsexplorer.Handler
|
||||
infraMonitoringHandler inframonitoring.Handler
|
||||
gatewayHandler gateway.Handler
|
||||
fieldsHandler fields.Handler
|
||||
authzHandler authz.Handler
|
||||
@@ -77,6 +79,7 @@ func NewFactory(
|
||||
dashboardModule dashboard.Module,
|
||||
dashboardHandler dashboard.Handler,
|
||||
metricsExplorerHandler metricsexplorer.Handler,
|
||||
infraMonitoringHandler inframonitoring.Handler,
|
||||
gatewayHandler gateway.Handler,
|
||||
fieldsHandler fields.Handler,
|
||||
authzHandler authz.Handler,
|
||||
@@ -108,6 +111,7 @@ func NewFactory(
|
||||
dashboardModule,
|
||||
dashboardHandler,
|
||||
metricsExplorerHandler,
|
||||
infraMonitoringHandler,
|
||||
gatewayHandler,
|
||||
fieldsHandler,
|
||||
authzHandler,
|
||||
@@ -141,6 +145,7 @@ func newProvider(
|
||||
dashboardModule dashboard.Module,
|
||||
dashboardHandler dashboard.Handler,
|
||||
metricsExplorerHandler metricsexplorer.Handler,
|
||||
infraMonitoringHandler inframonitoring.Handler,
|
||||
gatewayHandler gateway.Handler,
|
||||
fieldsHandler fields.Handler,
|
||||
authzHandler authz.Handler,
|
||||
@@ -172,6 +177,7 @@ func newProvider(
|
||||
dashboardModule: dashboardModule,
|
||||
dashboardHandler: dashboardHandler,
|
||||
metricsExplorerHandler: metricsExplorerHandler,
|
||||
infraMonitoringHandler: infraMonitoringHandler,
|
||||
gatewayHandler: gatewayHandler,
|
||||
fieldsHandler: fieldsHandler,
|
||||
authzHandler: authzHandler,
|
||||
@@ -240,6 +246,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addInfraMonitoringRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addGatewayRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
33
pkg/modules/inframonitoring/config.go
Normal file
33
pkg/modules/inframonitoring/config.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package inframonitoring
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
TelemetryStore TelemetryStoreConfig `mapstructure:"telemetrystore"`
|
||||
}
|
||||
|
||||
type TelemetryStoreConfig struct {
|
||||
Threads int `mapstructure:"threads"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("inframonitoring"), newConfig)
|
||||
}
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return Config{
|
||||
TelemetryStore: TelemetryStoreConfig{
|
||||
Threads: 8,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.TelemetryStore.Threads <= 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "inframonitoring.telemetrystore.threads must be positive, got %d", c.TelemetryStore.Threads)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
95
pkg/modules/inframonitoring/implinframonitoring/handler.go
Normal file
95
pkg/modules/inframonitoring/implinframonitoring/handler.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
module inframonitoring.Module
|
||||
}
|
||||
|
||||
// NewHandler returns an inframonitoring.Handler implementation.
|
||||
func NewHandler(m inframonitoring.Module) inframonitoring.Handler {
|
||||
return &handler{
|
||||
module: m,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) ListHosts(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.PostableHosts
|
||||
if err := binding.JSON.BindBody(req.Body, &parsedReq); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.ListHosts(req.Context(), orgID, &parsedReq)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) ListPods(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.PostablePods
|
||||
if err := binding.JSON.BindBody(req.Body, &parsedReq); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.ListPods(req.Context(), orgID, &parsedReq)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) GetOnboarding(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.PostableOnboarding
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), &parsedReq); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.GetOnboarding(req.Context(), orgID, &parsedReq)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
625
pkg/modules/inframonitoring/implinframonitoring/helpers.go
Normal file
625
pkg/modules/inframonitoring/implinframonitoring/helpers.go
Normal file
@@ -0,0 +1,625 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
)
|
||||
|
||||
// quoteIdentifier wraps s in backticks for use as a ClickHouse identifier,
|
||||
// escaping any embedded backticks by doubling them.
|
||||
func quoteIdentifier(s string) string {
|
||||
return fmt.Sprintf("`%s`", strings.ReplaceAll(s, "`", "``"))
|
||||
}
|
||||
|
||||
func isKeyInGroupByAttrs(groupByAttrs []qbtypes.GroupByKey, key string) bool {
|
||||
for _, groupBy := range groupByAttrs {
|
||||
if groupBy.Name == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mergeFilterExpressions(queryFilterExpr, reqFilterExpr string) string {
|
||||
queryFilterExpr = strings.TrimSpace(queryFilterExpr)
|
||||
reqFilterExpr = strings.TrimSpace(reqFilterExpr)
|
||||
if queryFilterExpr == "" {
|
||||
return reqFilterExpr
|
||||
}
|
||||
if reqFilterExpr == "" {
|
||||
return queryFilterExpr
|
||||
}
|
||||
return fmt.Sprintf("(%s) AND (%s)", queryFilterExpr, reqFilterExpr)
|
||||
}
|
||||
|
||||
// compositeKeyFromList builds a composite key by joining the given parts
|
||||
// with a null byte separator. This is the canonical way to construct
|
||||
// composite keys for group identification across the infra monitoring module.
|
||||
func compositeKeyFromList(parts []string) string {
|
||||
return strings.Join(parts, "\x00")
|
||||
}
|
||||
|
||||
// compositeKeyFromLabels builds a composite key from a label map by extracting
|
||||
// the value for each groupBy key in order and joining them via compositeKeyFromList.
|
||||
func compositeKeyFromLabels(labels map[string]string, groupBy []qbtypes.GroupByKey) string {
|
||||
parts := make([]string, len(groupBy))
|
||||
for i, key := range groupBy {
|
||||
parts[i] = labels[key.Name]
|
||||
}
|
||||
return compositeKeyFromList(parts)
|
||||
}
|
||||
|
||||
// parseAndSortGroups extracts group label maps from a ScalarData response and
|
||||
// sorts them by the ranking query's aggregation value.
|
||||
func parseAndSortGroups(
|
||||
resp *qbtypes.QueryRangeResponse,
|
||||
rankingQueryName string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
direction qbtypes.OrderDirection,
|
||||
) []rankedGroup {
|
||||
if resp == nil || len(resp.Data.Results) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the ScalarData that contains the ranking column.
|
||||
var sd *qbtypes.ScalarData
|
||||
for _, r := range resp.Data.Results {
|
||||
candidate, ok := r.(*qbtypes.ScalarData)
|
||||
if !ok || candidate == nil {
|
||||
continue
|
||||
}
|
||||
for _, col := range candidate.Columns {
|
||||
if col.Type == qbtypes.ColumnTypeAggregation && col.QueryName == rankingQueryName {
|
||||
sd = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if sd != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if sd == nil || len(sd.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
groupColIndices := make(map[string]int)
|
||||
rankingColIdx := -1
|
||||
for i, col := range sd.Columns {
|
||||
if col.Type == qbtypes.ColumnTypeGroup {
|
||||
groupColIndices[col.Name] = i
|
||||
}
|
||||
if col.Type == qbtypes.ColumnTypeAggregation && col.QueryName == rankingQueryName {
|
||||
rankingColIdx = i
|
||||
}
|
||||
}
|
||||
if rankingColIdx == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
groups := make([]rankedGroup, 0, len(sd.Data))
|
||||
for _, row := range sd.Data {
|
||||
labels := make(map[string]string, len(groupBy))
|
||||
for _, key := range groupBy {
|
||||
if idx, ok := groupColIndices[key.Name]; ok && idx < len(row) {
|
||||
labels[key.Name] = fmt.Sprintf("%v", row[idx])
|
||||
}
|
||||
}
|
||||
var value float64
|
||||
if rankingColIdx < len(row) {
|
||||
if v, ok := row[rankingColIdx].(float64); ok {
|
||||
value = v
|
||||
}
|
||||
}
|
||||
groups = append(groups, rankedGroup{
|
||||
labels: labels,
|
||||
value: value,
|
||||
compositeKey: compositeKeyFromLabels(labels, groupBy),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(groups, func(i, j int) bool {
|
||||
if groups[i].value != groups[j].value {
|
||||
if direction == qbtypes.OrderDirectionAsc {
|
||||
return groups[i].value < groups[j].value
|
||||
}
|
||||
return groups[i].value > groups[j].value
|
||||
}
|
||||
return groups[i].compositeKey < groups[j].compositeKey
|
||||
})
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// paginateWithBackfill returns the page of groups for [offset, offset+limit).
|
||||
// The virtual sorted list is: metric-ranked groups first, then metadata-only
|
||||
// groups (those in metadataMap but not in metric results) sorted alphabetically.
|
||||
func paginateWithBackfill(
|
||||
metricGroups []rankedGroup,
|
||||
metadataMap map[string]map[string]string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
offset, limit int,
|
||||
) []map[string]string {
|
||||
metricKeySet := make(map[string]bool, len(metricGroups))
|
||||
for _, g := range metricGroups {
|
||||
metricKeySet[g.compositeKey] = true
|
||||
}
|
||||
|
||||
metadataOnlyKeys := make([]string, 0)
|
||||
for compositeKey := range metadataMap {
|
||||
if !metricKeySet[compositeKey] {
|
||||
metadataOnlyKeys = append(metadataOnlyKeys, compositeKey)
|
||||
}
|
||||
}
|
||||
sort.Strings(metadataOnlyKeys)
|
||||
|
||||
totalMetric := len(metricGroups)
|
||||
totalAll := totalMetric + len(metadataOnlyKeys)
|
||||
|
||||
end := offset + limit
|
||||
if end > totalAll {
|
||||
end = totalAll
|
||||
}
|
||||
if offset >= totalAll {
|
||||
return nil
|
||||
}
|
||||
|
||||
pageGroups := make([]map[string]string, 0, end-offset)
|
||||
for i := offset; i < end; i++ {
|
||||
if i < totalMetric {
|
||||
pageGroups = append(pageGroups, metricGroups[i].labels)
|
||||
} else {
|
||||
compositeKey := metadataOnlyKeys[i-totalMetric]
|
||||
attrs := metadataMap[compositeKey]
|
||||
labels := make(map[string]string, len(groupBy))
|
||||
for _, key := range groupBy {
|
||||
labels[key.Name] = attrs[key.Name]
|
||||
}
|
||||
pageGroups = append(pageGroups, labels)
|
||||
}
|
||||
}
|
||||
return pageGroups
|
||||
}
|
||||
|
||||
// buildPageGroupsFilterExpr builds a filter expression that restricts results
|
||||
// to the given page of groups via IN clauses.
|
||||
// Returns e.g. "host.name IN ('h1','h2') AND os.type IN ('linux','windows')".
|
||||
func buildPageGroupsFilterExpr(pageGroups []map[string]string) string {
|
||||
groupValues := make(map[string][]string)
|
||||
for _, labels := range pageGroups {
|
||||
for k, v := range labels {
|
||||
groupValues[k] = append(groupValues[k], v)
|
||||
}
|
||||
}
|
||||
|
||||
inClauses := make([]string, 0, len(groupValues))
|
||||
for key, values := range groupValues {
|
||||
quoted := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
quoted[i] = fmt.Sprintf("'%s'", v)
|
||||
}
|
||||
inClauses = append(inClauses, fmt.Sprintf("%s IN (%s)", key, strings.Join(quoted, ", ")))
|
||||
}
|
||||
return strings.Join(inClauses, " AND ")
|
||||
}
|
||||
|
||||
// buildFullQueryRequest creates a QueryRangeRequest for all metrics,
|
||||
// restricted to the given page of groups via an IN filter.
|
||||
// Accepts primitive fields so it can be reused across different v2 APIs
|
||||
// (hosts, pods, etc.).
|
||||
func buildFullQueryRequest(
|
||||
start int64,
|
||||
end int64,
|
||||
filterExpr string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
tableListQuery *qbtypes.QueryRangeRequest,
|
||||
) *qbtypes.QueryRangeRequest {
|
||||
inFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
|
||||
fullReq := &qbtypes.QueryRangeRequest{
|
||||
Start: uint64(start),
|
||||
End: uint64(end),
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: make([]qbtypes.QueryEnvelope, 0, len(tableListQuery.CompositeQuery.Queries)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, envelope := range tableListQuery.CompositeQuery.Queries {
|
||||
copied := envelope
|
||||
if copied.Type == qbtypes.QueryTypeBuilder {
|
||||
existingExpr := ""
|
||||
if f := copied.GetFilter(); f != nil {
|
||||
existingExpr = f.Expression
|
||||
}
|
||||
merged := mergeFilterExpressions(existingExpr, filterExpr)
|
||||
merged = mergeFilterExpressions(merged, inFilterExpr)
|
||||
copied.SetFilter(&qbtypes.Filter{Expression: merged})
|
||||
copied.SetGroupBy(groupBy)
|
||||
}
|
||||
fullReq.CompositeQuery.Queries = append(fullReq.CompositeQuery.Queries, copied)
|
||||
}
|
||||
|
||||
return fullReq
|
||||
}
|
||||
|
||||
// parseFullQueryResponse extracts per-group metric values from the full
|
||||
// composite query response. Returns compositeKey -> (queryName -> value).
|
||||
// Each enabled query/formula produces its own ScalarData entry in Results,
|
||||
// so we iterate over all of them and merge metrics per composite key.
|
||||
func parseFullQueryResponse(
|
||||
resp *qbtypes.QueryRangeResponse,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
) map[string]map[string]float64 {
|
||||
result := make(map[string]map[string]float64)
|
||||
if resp == nil || len(resp.Data.Results) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, r := range resp.Data.Results {
|
||||
sd, ok := r.(*qbtypes.ScalarData)
|
||||
if !ok || sd == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
groupColIndices := make(map[string]int)
|
||||
aggCols := make(map[int]string) // col index -> query name
|
||||
for i, col := range sd.Columns {
|
||||
if col.Type == qbtypes.ColumnTypeGroup {
|
||||
groupColIndices[col.Name] = i
|
||||
}
|
||||
if col.Type == qbtypes.ColumnTypeAggregation {
|
||||
aggCols[i] = col.QueryName
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range sd.Data {
|
||||
labels := make(map[string]string, len(groupBy))
|
||||
for _, key := range groupBy {
|
||||
if idx, ok := groupColIndices[key.Name]; ok && idx < len(row) {
|
||||
labels[key.Name] = fmt.Sprintf("%v", row[idx])
|
||||
}
|
||||
}
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
|
||||
if result[compositeKey] == nil {
|
||||
result[compositeKey] = make(map[string]float64)
|
||||
}
|
||||
for idx, queryName := range aggCols {
|
||||
if idx < len(row) {
|
||||
if v, ok := row[idx].(float64); ok {
|
||||
result[compositeKey][queryName] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// buildSamplesTblFingerprintSubQuery returns a SelectBuilder that selects distinct fingerprints
|
||||
// from the samples table for the given metric names andtime range.
|
||||
func (m *module) buildSamplesTblFingerprintSubQuery(metricNames []string, startMs, endMs int64) *sqlbuilder.SelectBuilder {
|
||||
samplesTableName := telemetrymetrics.WhichSamplesTableToUse(
|
||||
uint64(startMs), uint64(endMs),
|
||||
metrictypes.UnspecifiedType,
|
||||
metrictypes.TimeAggregationUnspecified,
|
||||
nil,
|
||||
)
|
||||
localSamplesTable := strings.TrimPrefix(samplesTableName, "distributed_")
|
||||
fpSB := sqlbuilder.NewSelectBuilder()
|
||||
fpSB.Select("DISTINCT fingerprint")
|
||||
fpSB.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localSamplesTable))
|
||||
fpSB.Where(
|
||||
fpSB.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
fpSB.GE("unix_milli", startMs),
|
||||
fpSB.L("unix_milli", endMs),
|
||||
)
|
||||
return fpSB
|
||||
}
|
||||
|
||||
func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter, startMillis, endMillis int64) (*sqlbuilder.WhereClause, error) {
|
||||
expression := ""
|
||||
if filter != nil {
|
||||
expression = strings.TrimSpace(filter.Expression)
|
||||
}
|
||||
if expression == "" {
|
||||
return sqlbuilder.NewWhereClause(), nil
|
||||
}
|
||||
|
||||
whereClauseSelectors := querybuilder.QueryStringToKeysSelectors(expression)
|
||||
for idx := range whereClauseSelectors {
|
||||
whereClauseSelectors[idx].Signal = telemetrytypes.SignalMetrics
|
||||
whereClauseSelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
|
||||
}
|
||||
|
||||
keys, _, err := m.telemetryMetadataStore.GetKeysMulti(ctx, whereClauseSelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := querybuilder.FilterExprVisitorOpts{
|
||||
Context: ctx,
|
||||
Logger: m.logger,
|
||||
FieldMapper: m.fieldMapper,
|
||||
ConditionBuilder: m.condBuilder,
|
||||
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "metric_name", FieldContext: telemetrytypes.FieldContextMetric},
|
||||
FieldKeys: keys,
|
||||
StartNs: querybuilder.ToNanoSecs(uint64(startMillis)),
|
||||
EndNs: querybuilder.ToNanoSecs(uint64(endMillis)),
|
||||
}
|
||||
|
||||
whereClause, err := querybuilder.PrepareWhereClause(expression, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if whereClause == nil || whereClause.WhereClause == nil {
|
||||
return sqlbuilder.NewWhereClause(), nil
|
||||
}
|
||||
|
||||
return whereClause.WhereClause, nil
|
||||
}
|
||||
|
||||
// NOTE: this method is not specific to infra monitoring — it queries attributes_metadata generically.
|
||||
// Consider moving to telemetryMetaStore when a second use case emerges.
|
||||
//
|
||||
// getMetricsExistenceAndEarliestTime checks which of the given metric names have been
|
||||
// reported. It returns a list of missing metrics (those not found or with zero count)
|
||||
// and the earliest first-reported timestamp across all present metrics.
|
||||
// When all metrics are missing, minFirstReportedUnixMilli is 0.
|
||||
// TODO(nikhilmantri0902, srikanthccv): This method was designed this way because querier errors if any of the metrics
|
||||
// in the querier list was never sent, the QueryRange call throws not found error. Modify this method, if QueryRange
|
||||
// behaviour changes towards this.
|
||||
func (m *module) getMetricsExistenceAndEarliestTime(ctx context.Context, metricNames []string) ([]string, uint64, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("metric_name", "count(*) AS cnt", "min(first_reported_unix_milli) AS min_first_reported")
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
|
||||
sb.Where(sb.In("metric_name", sqlbuilder.List(metricNames)))
|
||||
sb.GroupBy("metric_name")
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type metricInfo struct {
|
||||
count uint64
|
||||
minFirstReported uint64
|
||||
}
|
||||
found := make(map[string]metricInfo, len(metricNames))
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var cnt, minFR uint64
|
||||
if err := rows.Scan(&name, &cnt, &minFR); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
found[name] = metricInfo{count: cnt, minFirstReported: minFR}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var missingMetrics []string
|
||||
var globalMinFirstReported uint64
|
||||
for _, name := range metricNames {
|
||||
info, ok := found[name]
|
||||
if !ok || info.count == 0 {
|
||||
missingMetrics = append(missingMetrics, name)
|
||||
continue
|
||||
}
|
||||
if globalMinFirstReported == 0 || info.minFirstReported < globalMinFirstReported {
|
||||
globalMinFirstReported = info.minFirstReported
|
||||
}
|
||||
}
|
||||
|
||||
return missingMetrics, globalMinFirstReported, nil
|
||||
}
|
||||
|
||||
// getAttributesPresence returns the subset of attrNames that have ever been
|
||||
// reported as a label on any of the given metricNames. The result maps each
|
||||
// present attr name to true; attrs not in the map are absent.
|
||||
//
|
||||
// Presence is checked against distributed_metadata without a time-range
|
||||
// filter — the metadata table is append-only, so an attr that ever existed
|
||||
// for a metric stays visible. Callers decide "required but missing" via
|
||||
// lookup against their required list.
|
||||
//
|
||||
// Sibling to getMetricsExistenceAndEarliestTime — used by the onboarding API
|
||||
// to validate required resource attributes (e.g. host.name, k8s.pod.uid) for
|
||||
// each infra-monitoring tab.
|
||||
func (m *module) getAttributesPresence(ctx context.Context, metricNames, attrNames []string) (map[string]bool, error) {
|
||||
if len(metricNames) == 0 || len(attrNames) == 0 {
|
||||
return map[string]bool{}, nil
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("attr_name", "count(*) AS cnt")
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
|
||||
sb.Where(
|
||||
sb.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
sb.In("attr_name", sqlbuilder.List(attrNames)),
|
||||
)
|
||||
sb.GroupBy("attr_name")
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
present := make(map[string]bool, len(attrNames))
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var cnt uint64
|
||||
if err := rows.Scan(&name, &cnt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name != "" && cnt > 0 {
|
||||
present[name] = true
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return present, nil
|
||||
}
|
||||
|
||||
// getMetadata fetches the latest values of additionalCols for each unique combination of groupBy keys,
|
||||
// within the given time range and metric names. It uses argMax(tuple(...), unix_milli) to ensure
|
||||
// we always pick attribute values from the latest timestamp for each group.
|
||||
// The returned map has a composite key of groupBy column values joined by "\x00" (null byte),
|
||||
// mapping to a flat map of attr_name -> attr_value (includes both groupBy and additional cols).
|
||||
func (m *module) getMetadata(
|
||||
ctx context.Context,
|
||||
metricNames []string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
additionalCols []string,
|
||||
filter *qbtypes.Filter,
|
||||
startMs, endMs int64,
|
||||
) (map[string]map[string]string, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricNames must not be empty")
|
||||
}
|
||||
if len(groupBy) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "groupBy must not be empty")
|
||||
}
|
||||
|
||||
// Pick the optimal timeseries table based on time range; also get adjusted start.
|
||||
adjustedStart, adjustedEnd, distributedTableName, _ := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(startMs), uint64(endMs), nil,
|
||||
)
|
||||
|
||||
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, startMs, endMs)
|
||||
|
||||
// Flatten groupBy keys to string names for SQL expressions and result scanning.
|
||||
groupByCols := make([]string, len(groupBy))
|
||||
for i, key := range groupBy {
|
||||
groupByCols[i] = key.Name
|
||||
}
|
||||
allCols := append(groupByCols, additionalCols...)
|
||||
|
||||
// --- Build inner query ---
|
||||
innerSB := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
// Inner SELECT columns: JSONExtractString for each groupBy col + argMax(tuple(...)) for additional cols
|
||||
innerSelectCols := make([]string, 0, len(groupByCols)+1)
|
||||
for _, col := range groupByCols {
|
||||
innerSelectCols = append(innerSelectCols,
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", innerSB.Var(col), quoteIdentifier(col)),
|
||||
)
|
||||
}
|
||||
|
||||
// Build the argMax(tuple(...), unix_milli) expression for all additional cols
|
||||
if len(additionalCols) > 0 {
|
||||
tupleArgs := make([]string, 0, len(additionalCols))
|
||||
for _, col := range additionalCols {
|
||||
tupleArgs = append(tupleArgs, fmt.Sprintf("JSONExtractString(labels, %s)", innerSB.Var(col)))
|
||||
}
|
||||
innerSelectCols = append(innerSelectCols,
|
||||
fmt.Sprintf("argMax(tuple(%s), unix_milli) AS latest_attrs", strings.Join(tupleArgs, ", ")),
|
||||
)
|
||||
}
|
||||
|
||||
innerSB.Select(innerSelectCols...)
|
||||
innerSB.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTableName))
|
||||
innerSB.Where(
|
||||
innerSB.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
innerSB.GE("unix_milli", adjustedStart),
|
||||
innerSB.L("unix_milli", adjustedEnd),
|
||||
fmt.Sprintf("fingerprint IN (%s)", innerSB.Var(fpSB)),
|
||||
)
|
||||
|
||||
// Apply optional filter expression
|
||||
if filter != nil && strings.TrimSpace(filter.Expression) != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, filter, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filterClause != nil {
|
||||
innerSB.AddWhereClause(filterClause)
|
||||
}
|
||||
}
|
||||
|
||||
groupByAliases := make([]string, 0, len(groupByCols))
|
||||
for _, col := range groupByCols {
|
||||
groupByAliases = append(groupByAliases, quoteIdentifier(col))
|
||||
}
|
||||
innerSB.GroupBy(groupByAliases...)
|
||||
|
||||
innerQuery, innerArgs := innerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// --- Build outer query ---
|
||||
// Outer SELECT columns: groupBy cols directly + tupleElement(latest_attrs, N) for each additionalCol
|
||||
outerSelectCols := make([]string, 0, len(allCols))
|
||||
for _, col := range groupByCols {
|
||||
outerSelectCols = append(outerSelectCols, quoteIdentifier(col))
|
||||
}
|
||||
for i, col := range additionalCols {
|
||||
outerSelectCols = append(outerSelectCols,
|
||||
fmt.Sprintf("tupleElement(latest_attrs, %d) AS %s", i+1, quoteIdentifier(col)),
|
||||
)
|
||||
}
|
||||
|
||||
outerSB := sqlbuilder.NewSelectBuilder()
|
||||
outerSB.Select(outerSelectCols...)
|
||||
outerSB.From(fmt.Sprintf("(%s)", innerQuery))
|
||||
|
||||
outerQuery, _ := outerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
// All ? params are in innerArgs; outer query introduces none of its own.
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, outerQuery, innerArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[string]map[string]string)
|
||||
|
||||
for rows.Next() {
|
||||
row := make([]string, len(allCols))
|
||||
scanPtrs := make([]any, len(row))
|
||||
for i := range row {
|
||||
scanPtrs[i] = &row[i]
|
||||
}
|
||||
|
||||
if err := rows.Scan(scanPtrs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
compositeKey := compositeKeyFromList(row[:len(groupByCols)])
|
||||
|
||||
attrMap := make(map[string]string, len(allCols))
|
||||
for i, col := range allCols {
|
||||
attrMap[col] = row[i]
|
||||
}
|
||||
result[compositeKey] = attrMap
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
283
pkg/modules/inframonitoring/implinframonitoring/helpers_test.go
Normal file
283
pkg/modules/inframonitoring/implinframonitoring/helpers_test.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
func groupByKey(name string) qbtypes.GroupByKey {
|
||||
return qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: name},
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsKeyInGroupByAttrs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
groupByAttrs []qbtypes.GroupByKey
|
||||
key string
|
||||
expectedFound bool
|
||||
}{
|
||||
{
|
||||
name: "key present in single-element list",
|
||||
groupByAttrs: []qbtypes.GroupByKey{groupByKey("host.name")},
|
||||
key: "host.name",
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "key present in multi-element list",
|
||||
groupByAttrs: []qbtypes.GroupByKey{
|
||||
groupByKey("host.name"),
|
||||
groupByKey("os.type"),
|
||||
groupByKey("k8s.cluster.name"),
|
||||
},
|
||||
key: "os.type",
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "key at last position",
|
||||
groupByAttrs: []qbtypes.GroupByKey{
|
||||
groupByKey("host.name"),
|
||||
groupByKey("os.type"),
|
||||
},
|
||||
key: "os.type",
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "key not in list",
|
||||
groupByAttrs: []qbtypes.GroupByKey{groupByKey("host.name")},
|
||||
key: "os.type",
|
||||
expectedFound: false,
|
||||
},
|
||||
{
|
||||
name: "empty group by list",
|
||||
groupByAttrs: []qbtypes.GroupByKey{},
|
||||
key: "host.name",
|
||||
expectedFound: false,
|
||||
},
|
||||
{
|
||||
name: "nil group by list",
|
||||
groupByAttrs: nil,
|
||||
key: "host.name",
|
||||
expectedFound: false,
|
||||
},
|
||||
{
|
||||
name: "empty key string",
|
||||
groupByAttrs: []qbtypes.GroupByKey{groupByKey("host.name")},
|
||||
key: "",
|
||||
expectedFound: false,
|
||||
},
|
||||
{
|
||||
name: "empty key matches empty-named group by key",
|
||||
groupByAttrs: []qbtypes.GroupByKey{groupByKey("")},
|
||||
key: "",
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "partial match does not count",
|
||||
groupByAttrs: []qbtypes.GroupByKey{
|
||||
groupByKey("host"),
|
||||
},
|
||||
key: "host.name",
|
||||
expectedFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isKeyInGroupByAttrs(tt.groupByAttrs, tt.key)
|
||||
if got != tt.expectedFound {
|
||||
t.Errorf("isKeyInGroupByAttrs(%v, %q) = %v, want %v",
|
||||
tt.groupByAttrs, tt.key, got, tt.expectedFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeFilterExpressions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
queryFilterExpr string
|
||||
reqFilterExpr string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "both non-empty",
|
||||
queryFilterExpr: "cpu > 50",
|
||||
reqFilterExpr: "host.name = 'web-1'",
|
||||
expected: "(cpu > 50) AND (host.name = 'web-1')",
|
||||
},
|
||||
{
|
||||
name: "query empty, req non-empty",
|
||||
queryFilterExpr: "",
|
||||
reqFilterExpr: "host.name = 'web-1'",
|
||||
expected: "host.name = 'web-1'",
|
||||
},
|
||||
{
|
||||
name: "query non-empty, req empty",
|
||||
queryFilterExpr: "cpu > 50",
|
||||
reqFilterExpr: "",
|
||||
expected: "cpu > 50",
|
||||
},
|
||||
{
|
||||
name: "both empty",
|
||||
queryFilterExpr: "",
|
||||
reqFilterExpr: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "whitespace-only query treated as empty",
|
||||
queryFilterExpr: " ",
|
||||
reqFilterExpr: "host.name = 'web-1'",
|
||||
expected: "host.name = 'web-1'",
|
||||
},
|
||||
{
|
||||
name: "whitespace-only req treated as empty",
|
||||
queryFilterExpr: "cpu > 50",
|
||||
reqFilterExpr: " ",
|
||||
expected: "cpu > 50",
|
||||
},
|
||||
{
|
||||
name: "both whitespace-only",
|
||||
queryFilterExpr: " ",
|
||||
reqFilterExpr: " ",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "leading/trailing whitespace trimmed before merge",
|
||||
queryFilterExpr: " cpu > 50 ",
|
||||
reqFilterExpr: " mem < 80 ",
|
||||
expected: "(cpu > 50) AND (mem < 80)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := mergeFilterExpressions(tt.queryFilterExpr, tt.reqFilterExpr)
|
||||
if got != tt.expected {
|
||||
t.Errorf("mergeFilterExpressions(%q, %q) = %q, want %q",
|
||||
tt.queryFilterExpr, tt.reqFilterExpr, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompositeKeyFromList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
parts []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "single part",
|
||||
parts: []string{"web-1"},
|
||||
expected: "web-1",
|
||||
},
|
||||
{
|
||||
name: "multiple parts joined with null separator",
|
||||
parts: []string{"web-1", "linux", "us-east"},
|
||||
expected: "web-1\x00linux\x00us-east",
|
||||
},
|
||||
{
|
||||
name: "empty slice returns empty string",
|
||||
parts: []string{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "nil slice returns empty string",
|
||||
parts: nil,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "parts with empty strings",
|
||||
parts: []string{"web-1", "", "us-east"},
|
||||
expected: "web-1\x00\x00us-east",
|
||||
},
|
||||
{
|
||||
name: "all empty strings",
|
||||
parts: []string{"", ""},
|
||||
expected: "\x00",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := compositeKeyFromList(tt.parts)
|
||||
if got != tt.expected {
|
||||
t.Errorf("compositeKeyFromList(%v) = %q, want %q",
|
||||
tt.parts, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompositeKeyFromLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
labels map[string]string
|
||||
groupBy []qbtypes.GroupByKey
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "single group-by key",
|
||||
labels: map[string]string{"host.name": "web-1"},
|
||||
groupBy: []qbtypes.GroupByKey{groupByKey("host.name")},
|
||||
expected: "web-1",
|
||||
},
|
||||
{
|
||||
name: "multiple group-by keys joined with null separator",
|
||||
labels: map[string]string{
|
||||
"host.name": "web-1",
|
||||
"os.type": "linux",
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{groupByKey("host.name"), groupByKey("os.type")},
|
||||
expected: "web-1\x00linux",
|
||||
},
|
||||
{
|
||||
name: "missing label yields empty segment",
|
||||
labels: map[string]string{"host.name": "web-1"},
|
||||
groupBy: []qbtypes.GroupByKey{groupByKey("host.name"), groupByKey("os.type")},
|
||||
expected: "web-1\x00",
|
||||
},
|
||||
{
|
||||
name: "empty labels map",
|
||||
labels: map[string]string{},
|
||||
groupBy: []qbtypes.GroupByKey{groupByKey("host.name")},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty group-by slice",
|
||||
labels: map[string]string{"host.name": "web-1"},
|
||||
groupBy: []qbtypes.GroupByKey{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "nil labels map",
|
||||
labels: nil,
|
||||
groupBy: []qbtypes.GroupByKey{groupByKey("host.name")},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "order matches group-by order, not map iteration order",
|
||||
labels: map[string]string{
|
||||
"z": "last",
|
||||
"a": "first",
|
||||
"m": "middle",
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{groupByKey("a"), groupByKey("m"), groupByKey("z")},
|
||||
expected: "first\x00middle\x00last",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := compositeKeyFromLabels(tt.labels, tt.groupBy)
|
||||
if got != tt.expected {
|
||||
t.Errorf("compositeKeyFromLabels(%v, %v) = %q, want %q",
|
||||
tt.labels, tt.groupBy, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
354
pkg/modules/inframonitoring/implinframonitoring/hosts.go
Normal file
354
pkg/modules/inframonitoring/implinframonitoring/hosts.go
Normal file
@@ -0,0 +1,354 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
)
|
||||
|
||||
// getPerGroupHostStatusCounts computes the number of active and inactive hosts per group
|
||||
// for the current page. It queries the timeseries table using the provided filter
|
||||
// expression (which includes user filter + status filter + page groups IN clause).
|
||||
// Uses GLOBAL IN with the active-hosts subquery inside uniqExactIf for active count,
|
||||
// and a simple uniqExactIf for total count. Inactive = total - active (computed in Go).
|
||||
func (m *module) getPerGroupHostStatusCounts(
|
||||
ctx context.Context,
|
||||
req *inframonitoringtypes.PostableHosts,
|
||||
metricNames []string,
|
||||
pageGroups []map[string]string,
|
||||
sinceUnixMilli int64,
|
||||
) (map[string]groupHostStatusCounts, error) {
|
||||
|
||||
// Build the full filter expression from req (user filter + status filter) and page groups.
|
||||
reqFilterExpr := ""
|
||||
if req.Filter != nil {
|
||||
reqFilterExpr = req.Filter.Expression
|
||||
}
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
filterExpr := mergeFilterExpressions(reqFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
adjustedStart, adjustedEnd, distributedTimeSeriesTableName, _ := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(req.Start), uint64(req.End), nil,
|
||||
)
|
||||
|
||||
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", hostNameAttrKey)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
selectCols := make([]string, 0, len(req.GroupBy)+2)
|
||||
for _, key := range req.GroupBy {
|
||||
selectCols = append(selectCols,
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", sb.Var(key.Name), quoteIdentifier(key.Name)),
|
||||
)
|
||||
}
|
||||
|
||||
activeHostsSQ := m.getActiveHostsQuery(metricNames, hostNameAttrKey, sinceUnixMilli)
|
||||
selectCols = append(selectCols,
|
||||
fmt.Sprintf("uniqExactIf(%s, %s GLOBAL IN (%s)) AS active_host_count", hostNameExpr, hostNameExpr, sb.Var(activeHostsSQ)),
|
||||
fmt.Sprintf("uniqExactIf(%s, %s != '') AS total_host_count", hostNameExpr, hostNameExpr),
|
||||
)
|
||||
|
||||
// Build a fingerprint subquery to restrict to fingerprints with actual sample
|
||||
// data in the original time range (not the wider timeseries table window).
|
||||
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, req.Start, req.End)
|
||||
|
||||
sb.Select(selectCols...)
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTimeSeriesTableName))
|
||||
sb.Where(
|
||||
sb.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
sb.GE("unix_milli", adjustedStart),
|
||||
sb.L("unix_milli", adjustedEnd),
|
||||
fmt.Sprintf("fingerprint IN (%s)", sb.Var(fpSB)),
|
||||
)
|
||||
|
||||
// Apply the combined filter expression (user filter + status filter + page groups IN).
|
||||
if filterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filterClause != nil {
|
||||
sb.AddWhereClause(filterClause)
|
||||
}
|
||||
}
|
||||
|
||||
// GROUP BY
|
||||
groupByAliases := make([]string, 0, len(req.GroupBy))
|
||||
for _, key := range req.GroupBy {
|
||||
groupByAliases = append(groupByAliases, quoteIdentifier(key.Name))
|
||||
}
|
||||
sb.GroupBy(groupByAliases...)
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[string]groupHostStatusCounts)
|
||||
for rows.Next() {
|
||||
groupVals := make([]string, len(req.GroupBy))
|
||||
scanPtrs := make([]any, 0, len(req.GroupBy)+2)
|
||||
for i := range groupVals {
|
||||
scanPtrs = append(scanPtrs, &groupVals[i])
|
||||
}
|
||||
|
||||
var activeCount, totalCount uint64
|
||||
scanPtrs = append(scanPtrs, &activeCount, &totalCount)
|
||||
|
||||
if err := rows.Scan(scanPtrs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
compositeKey := compositeKeyFromList(groupVals)
|
||||
result[compositeKey] = groupHostStatusCounts{
|
||||
Active: int(activeCount),
|
||||
Inactive: int(totalCount - activeCount),
|
||||
}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// buildHostRecords constructs the final list of HostRecords for a page.
|
||||
// Groups that had no metric data get default values of -1.
|
||||
//
|
||||
// hostCounts is nil when host.name is in the groupBy — in that case, counts are
|
||||
// derived directly from activeHostsMap (1/0 per host). When non-nil (custom groupBy
|
||||
// without host.name), counts are looked up from the map.
|
||||
func buildHostRecords(
|
||||
isHostNameInGroupBy bool,
|
||||
resp *qbtypes.QueryRangeResponse,
|
||||
pageGroups []map[string]string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
metadataMap map[string]map[string]string,
|
||||
activeHostsMap map[string]bool,
|
||||
hostCounts map[string]groupHostStatusCounts,
|
||||
) []inframonitoringtypes.HostRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
records := make([]inframonitoringtypes.HostRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
hostName := labels[hostNameAttrKey]
|
||||
|
||||
activeStatus := inframonitoringtypes.HostStatusNone
|
||||
activeHostCount := 0
|
||||
inactiveHostCount := 0
|
||||
if isHostNameInGroupBy { // derive from activeHostsMap since each row = one host
|
||||
if hostName != "" {
|
||||
if activeHostsMap[hostName] {
|
||||
activeStatus = inframonitoringtypes.HostStatusActive
|
||||
activeHostCount = 1
|
||||
} else {
|
||||
activeStatus = inframonitoringtypes.HostStatusInactive
|
||||
inactiveHostCount = 1
|
||||
}
|
||||
}
|
||||
} else { // derive from hostCounts since custom groupBy without host.name
|
||||
if counts, ok := hostCounts[compositeKey]; ok {
|
||||
activeHostCount = counts.Active
|
||||
inactiveHostCount = counts.Inactive
|
||||
}
|
||||
}
|
||||
|
||||
record := inframonitoringtypes.HostRecord{
|
||||
HostName: hostName,
|
||||
Status: activeStatus,
|
||||
ActiveHostCount: activeHostCount,
|
||||
InactiveHostCount: inactiveHostCount,
|
||||
CPU: -1,
|
||||
Memory: -1,
|
||||
Wait: -1,
|
||||
Load15: -1,
|
||||
DiskUsage: -1,
|
||||
Meta: map[string]any{},
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
if v, exists := metrics["F1"]; exists {
|
||||
record.CPU = v
|
||||
}
|
||||
if v, exists := metrics["F2"]; exists {
|
||||
record.Memory = v
|
||||
}
|
||||
if v, exists := metrics["F3"]; exists {
|
||||
record.Wait = v
|
||||
}
|
||||
if v, exists := metrics["F4"]; exists {
|
||||
record.DiskUsage = v
|
||||
}
|
||||
if v, exists := metrics["G"]; exists {
|
||||
record.Load15 = v
|
||||
}
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopHostGroups runs a ranking query for the ordering metric, sorts the
|
||||
// results, paginates, and backfills from metadataMap when the page extends
|
||||
// past the metric-ranked groups.
|
||||
func (m *module) getTopHostGroups(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableHosts,
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
queryNamesForOrderBy := orderByToHostsQueryNames[orderByKey]
|
||||
// The last entry is the formula/query whose value we sort by.
|
||||
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.newListHostsQuery().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
|
||||
}
|
||||
|
||||
// applyHostsActiveStatusFilter MODIFIES req.Filter.Expression to include an IN/NOT IN
|
||||
// clause based on FilterByStatus and the set of active hosts.
|
||||
// Returns true if the caller should short-circuit with an empty result (eg. ACTIVE
|
||||
// requested but no hosts are active).
|
||||
func (m *module) applyHostsActiveStatusFilter(req *inframonitoringtypes.PostableHosts, activeHostsMap map[string]bool) (shouldShortCircuit bool) {
|
||||
if req.Filter == nil || (req.Filter.FilterByStatus != inframonitoringtypes.HostStatusActive && req.Filter.FilterByStatus != inframonitoringtypes.HostStatusInactive) {
|
||||
return false
|
||||
}
|
||||
|
||||
activeHosts := make([]string, 0, len(activeHostsMap))
|
||||
for host := range activeHostsMap {
|
||||
activeHosts = append(activeHosts, fmt.Sprintf("'%s'", host))
|
||||
}
|
||||
|
||||
if len(activeHosts) == 0 {
|
||||
return req.Filter.FilterByStatus == inframonitoringtypes.HostStatusActive
|
||||
}
|
||||
|
||||
op := "IN"
|
||||
if req.Filter.FilterByStatus == inframonitoringtypes.HostStatusInactive {
|
||||
op = "NOT IN"
|
||||
}
|
||||
statusClause := fmt.Sprintf("%s %s (%s)", hostNameAttrKey, op, strings.Join(activeHosts, ", "))
|
||||
req.Filter.Expression = mergeFilterExpressions(req.Filter.Expression, statusClause)
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *module) getHostsTableMetadata(ctx context.Context, req *inframonitoringtypes.PostableHosts) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range hostAttrKeysForMetadata {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
metadataMap, err := m.getMetadata(ctx, hostsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return metadataMap, nil
|
||||
}
|
||||
|
||||
// getActiveHostsQuery builds a SelectBuilder that returns distinct host names
|
||||
// with metrics reported in the last 10 minutes. The builder is not executed —
|
||||
// callers can either execute it (getActiveHosts) or embed it as a subquery
|
||||
// (getPerGroupActiveInactiveHostCounts).
|
||||
func (m *module) getActiveHostsQuery(metricNames []string, hostNameAttr string, sinceUnixMilli int64) *sqlbuilder.SelectBuilder {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Distinct()
|
||||
sb.Select("attr_string_value")
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
|
||||
sb.Where(
|
||||
sb.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
sb.E("attr_name", hostNameAttr),
|
||||
sb.NE("attr_string_value", ""),
|
||||
sb.GE("last_reported_unix_milli", sinceUnixMilli),
|
||||
)
|
||||
|
||||
return sb
|
||||
}
|
||||
|
||||
// getActiveHosts returns a set of host names that have reported metrics recently.
|
||||
// It queries distributed_metadata for hosts where last_reported_unix_milli >= sinceUnixMilli.
|
||||
func (m *module) getActiveHosts(ctx context.Context, metricNames []string, hostNameAttr string, sinceUnixMilli int64) (map[string]bool, error) {
|
||||
sb := m.getActiveHostsQuery(metricNames, hostNameAttr, sinceUnixMilli)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
activeHosts := make(map[string]bool)
|
||||
for rows.Next() {
|
||||
var hostName string
|
||||
if err := rows.Scan(&hostName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hostName != "" {
|
||||
activeHosts[hostName] = true
|
||||
}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return activeHosts, nil
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
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 (
|
||||
hostNameAttrKey = "host.name"
|
||||
)
|
||||
|
||||
// Helper group-by key used across all queries.
|
||||
var hostNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: hostNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
}
|
||||
|
||||
var hostsTableMetricNamesList = []string{
|
||||
"system.cpu.time",
|
||||
"system.memory.usage",
|
||||
"system.cpu.load_average.15m",
|
||||
"system.filesystem.usage",
|
||||
}
|
||||
|
||||
var hostAttrKeysForMetadata = []string{
|
||||
"os.type",
|
||||
}
|
||||
|
||||
// orderByToHostsQueryNames maps the orderBy column to the query/formula names
|
||||
// from HostsTableListQuery used for ranking host groups.
|
||||
var orderByToHostsQueryNames = map[string][]string{
|
||||
inframonitoringtypes.HostsOrderByCPU: {"A", "B", "F1"},
|
||||
inframonitoringtypes.HostsOrderByMemory: {"C", "D", "F2"},
|
||||
inframonitoringtypes.HostsOrderByWait: {"E", "F", "F3"},
|
||||
inframonitoringtypes.HostsOrderByDiskUsage: {"H", "I", "F4"},
|
||||
inframonitoringtypes.HostsOrderByLoad15: {"G"},
|
||||
}
|
||||
|
||||
// newListHostsQuery constructs the base QueryRangeRequest with all the queries for the hosts table.
|
||||
// This is kept in this file because the queries themselves do not change based on the request parameters
|
||||
// only the filters, group bys, and order bys change, which are applied in buildFullQueryRequest.
|
||||
func (m *module) newListHostsQuery() *qbtypes.QueryRangeRequest {
|
||||
return &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
// Query A: CPU usage logic (non-idle)
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "system.cpu.time",
|
||||
TimeAggregation: metrictypes.TimeAggregationRate,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "state != 'idle'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{hostNameGroupByKey},
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
// Query B: CPU usage (all states)
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "system.cpu.time",
|
||||
TimeAggregation: metrictypes.TimeAggregationRate,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{hostNameGroupByKey},
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
// Formula F1: CPU Usage (%)
|
||||
{
|
||||
Type: qbtypes.QueryTypeFormula,
|
||||
Spec: qbtypes.QueryBuilderFormula{
|
||||
Name: "F1",
|
||||
Expression: "A/B",
|
||||
Legend: "CPU Usage (%)",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query C: Memory usage (state = used)
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "C",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "system.memory.usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "state = 'used'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{hostNameGroupByKey},
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
// Query D: Memory usage (all states)
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "D",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "system.memory.usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{hostNameGroupByKey},
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
// Formula F2: Memory Usage (%)
|
||||
{
|
||||
Type: qbtypes.QueryTypeFormula,
|
||||
Spec: qbtypes.QueryBuilderFormula{
|
||||
Name: "F2",
|
||||
Expression: "C/D",
|
||||
Legend: "Memory Usage (%)",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query E: CPU Wait time (state = wait)
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "E",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "system.cpu.time",
|
||||
TimeAggregation: metrictypes.TimeAggregationRate,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "state = 'wait'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{hostNameGroupByKey},
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
// Query F: CPU time (all states)
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "F",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "system.cpu.time",
|
||||
TimeAggregation: metrictypes.TimeAggregationRate,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{hostNameGroupByKey},
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
// Formula F3: CPU Wait Time (%)
|
||||
{
|
||||
Type: qbtypes.QueryTypeFormula,
|
||||
Spec: qbtypes.QueryBuilderFormula{
|
||||
Name: "F3",
|
||||
Expression: "E/F",
|
||||
Legend: "CPU Wait Time (%)",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query G: Load15
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "G",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Legend: "CPU Load Average (15m)",
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "system.cpu.load_average.15m",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{hostNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query H: Filesystem Usage (state = used)
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "H",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "system.filesystem.usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "state = 'used'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{hostNameGroupByKey},
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
// Query I: Filesystem Usage (all states)
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "I",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "system.filesystem.usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{hostNameGroupByKey},
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
// Formula F4: Disk Usage (%)
|
||||
{
|
||||
Type: qbtypes.QueryTypeFormula,
|
||||
Spec: qbtypes.QueryBuilderFormula{
|
||||
Name: "F4",
|
||||
Expression: "H/I",
|
||||
Legend: "Disk Usage (%)",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package implinframonitoring
|
||||
|
||||
// The types in this file are only used within the implinframonitoring package, and are not exposed outside.
|
||||
// They are primarily used for internal processing and structuring of data within the module's implementation.
|
||||
|
||||
type rankedGroup struct {
|
||||
labels map[string]string
|
||||
value float64
|
||||
compositeKey string
|
||||
}
|
||||
|
||||
// groupHostStatusCounts holds per-group active and inactive host counts.
|
||||
type groupHostStatusCounts struct {
|
||||
Active int
|
||||
Inactive int
|
||||
}
|
||||
237
pkg/modules/inframonitoring/implinframonitoring/module.go
Normal file
237
pkg/modules/inframonitoring/implinframonitoring/module.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
telemetryMetadataStore telemetrytypes.MetadataStore
|
||||
querier querier.Querier
|
||||
fieldMapper qbtypes.FieldMapper
|
||||
condBuilder qbtypes.ConditionBuilder
|
||||
logger *slog.Logger
|
||||
config inframonitoring.Config
|
||||
}
|
||||
|
||||
// NewModule constructs the inframonitoring module with the provided dependencies.
|
||||
func NewModule(
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
telemetryMetadataStore telemetrytypes.MetadataStore,
|
||||
querier querier.Querier,
|
||||
providerSettings factory.ProviderSettings,
|
||||
cfg inframonitoring.Config,
|
||||
) inframonitoring.Module {
|
||||
fieldMapper := telemetrymetrics.NewFieldMapper()
|
||||
condBuilder := telemetrymetrics.NewConditionBuilder(fieldMapper)
|
||||
return &module{
|
||||
telemetryStore: telemetryStore,
|
||||
telemetryMetadataStore: telemetryMetadataStore,
|
||||
querier: querier,
|
||||
fieldMapper: fieldMapper,
|
||||
condBuilder: condBuilder,
|
||||
logger: providerSettings.Logger,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableHosts) (*inframonitoringtypes.Hosts, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &inframonitoringtypes.Hosts{}
|
||||
|
||||
// default to cpu order by
|
||||
if req.OrderBy == nil {
|
||||
req.OrderBy = &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "cpu",
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
}
|
||||
}
|
||||
|
||||
// default to host name group by
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []qbtypes.GroupByKey{hostNameGroupByKey}
|
||||
resp.Type = inframonitoringtypes.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
// 1. Check which required metrics exist and get earliest retention time.
|
||||
// If any required metric is missing, return early with the list of missing metrics.
|
||||
// 2. If metrics exist but req.End is before the earliest reported time, return early with endTimeBeforeRetention=true.
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, hostsTableMetricNamesList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(missingMetrics) > 0 {
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
|
||||
resp.Records = []inframonitoringtypes.HostRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
if req.End < int64(minFirstReportedUnixMilli) {
|
||||
resp.EndTimeBeforeRetention = true
|
||||
resp.Records = []inframonitoringtypes.HostRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
|
||||
|
||||
// TOD(nikhilmantri0902): replace this separate ClickHouse query with a sub-query inside the main query builder query
|
||||
// once QB supports sub-queries.
|
||||
// Determine active hosts: those with metrics reported in the last 10 minutes.
|
||||
// Compute the cutoff once so every downstream query/subquery agrees on what "active" means.
|
||||
sinceUnixMilli := time.Now().Add(-10 * time.Minute).UTC().UnixMilli()
|
||||
activeHostsMap, err := m.getActiveHosts(ctx, hostsTableMetricNamesList, hostNameAttrKey, sinceUnixMilli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// this check below modifies req.Filter by adding `AND active hosts filter` if req.FilterByStatus is set.
|
||||
if m.applyHostsActiveStatusFilter(req, activeHostsMap) {
|
||||
resp.Records = []inframonitoringtypes.HostRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
metadataMap, err := m.getHostsTableMetadata(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
pageGroups, err := m.getTopHostGroups(ctx, orgID, req, metadataMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
resp.Records = []inframonitoringtypes.HostRecord{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
hostsFilterExpr := ""
|
||||
if req.Filter != nil {
|
||||
hostsFilterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, hostsFilterExpr, req.GroupBy, pageGroups, m.newListHostsQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compute per-group active/inactive host counts.
|
||||
// When host.name is in groupBy, each row = one host, so counts are derived
|
||||
// directly from activeHostsMap in buildHostRecords (no extra query needed).
|
||||
// When host.name is not in groupBy, we need to run an additional query to get the counts per group for the current page,
|
||||
// using the same filter expression as the main query (including user filters + page groups IN clause).
|
||||
hostCounts := make(map[string]groupHostStatusCounts)
|
||||
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, hostNameAttrKey)
|
||||
if !isHostNameInGroupBy {
|
||||
hostCounts, err = m.getPerGroupHostStatusCounts(ctx, req, hostsTableMetricNamesList, pageGroups, sinceUnixMilli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp.Records = buildHostRecords(isHostNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, activeHostsMap, hostCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostablePods) (*inframonitoringtypes.Pods, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &inframonitoringtypes.Pods{}
|
||||
|
||||
if req.OrderBy == nil {
|
||||
req.OrderBy = &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: inframonitoringtypes.PodsOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []qbtypes.GroupByKey{podUIDGroupByKey}
|
||||
resp.Type = inframonitoringtypes.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, podsTableMetricNamesList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(missingMetrics) > 0 {
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
|
||||
resp.Records = []inframonitoringtypes.PodRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
if req.End < int64(minFirstReportedUnixMilli) {
|
||||
resp.EndTimeBeforeRetention = true
|
||||
resp.Records = []inframonitoringtypes.PodRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
|
||||
|
||||
metadataMap, err := m.getPodsTableMetadata(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
pageGroups, err := m.getTopPodGroups(ctx, orgID, req, metadataMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
resp.Records = []inframonitoringtypes.PodRecord{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newPodsTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildPodRecords(queryResp, pageGroups, req.GroupBy, metadataMap, req.End)
|
||||
resp.Warning = queryResp.Warning
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
236
pkg/modules/inframonitoring/implinframonitoring/onboarding.go
Normal file
236
pkg/modules/inframonitoring/implinframonitoring/onboarding.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// GetOnboarding runs a per-type readiness check: for the requested
|
||||
// infra-monitoring tab, reports which required metrics and attributes are
|
||||
// present vs missing, grouped by the collector component that produces them.
|
||||
// Ready is true iff every missing list is empty.
|
||||
func (m *module) GetOnboarding(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableOnboarding) (*inframonitoringtypes.Onboarding, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spec, ok := onboardingSpecs[req.Type]
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "no onboarding spec for type: %s", req.Type)
|
||||
}
|
||||
|
||||
allMetrics, allAttrs := collectSpecUnions(spec)
|
||||
|
||||
missingMetricsList, _, err := m.getMetricsExistenceAndEarliestTime(ctx, allMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
missingMetrics := make(map[string]bool, len(missingMetricsList))
|
||||
for _, name := range missingMetricsList {
|
||||
missingMetrics[name] = true
|
||||
}
|
||||
|
||||
presentAttrs, err := m.getAttributesPresence(ctx, allMetrics, allAttrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &inframonitoringtypes.Onboarding{
|
||||
Type: req.Type,
|
||||
PresentDefaultEnabledMetrics: []inframonitoringtypes.MetricsComponentEntry{},
|
||||
PresentOptionalMetrics: []inframonitoringtypes.MetricsComponentEntry{},
|
||||
PresentRequiredAttributes: []inframonitoringtypes.AttributesComponentEntry{},
|
||||
MissingDefaultEnabledMetrics: []inframonitoringtypes.MissingMetricsComponentEntry{},
|
||||
MissingOptionalMetrics: []inframonitoringtypes.MissingMetricsComponentEntry{},
|
||||
MissingRequiredAttributes: []inframonitoringtypes.MissingAttributesComponentEntry{},
|
||||
}
|
||||
|
||||
for _, b := range spec.Buckets {
|
||||
s := splitBucket(b, missingMetrics, presentAttrs)
|
||||
if s.PresentDefault != nil {
|
||||
resp.PresentDefaultEnabledMetrics = append(resp.PresentDefaultEnabledMetrics, *s.PresentDefault)
|
||||
}
|
||||
if s.PresentOptional != nil {
|
||||
resp.PresentOptionalMetrics = append(resp.PresentOptionalMetrics, *s.PresentOptional)
|
||||
}
|
||||
if s.PresentAttrs != nil {
|
||||
resp.PresentRequiredAttributes = append(resp.PresentRequiredAttributes, *s.PresentAttrs)
|
||||
}
|
||||
if s.MissingDefault != nil {
|
||||
resp.MissingDefaultEnabledMetrics = append(resp.MissingDefaultEnabledMetrics, *s.MissingDefault)
|
||||
}
|
||||
if s.MissingOptional != nil {
|
||||
resp.MissingOptionalMetrics = append(resp.MissingOptionalMetrics, *s.MissingOptional)
|
||||
}
|
||||
if s.MissingAttrs != nil {
|
||||
resp.MissingRequiredAttributes = append(resp.MissingRequiredAttributes, *s.MissingAttrs)
|
||||
}
|
||||
}
|
||||
|
||||
resp.Ready = len(resp.MissingDefaultEnabledMetrics) == 0 &&
|
||||
len(resp.MissingOptionalMetrics) == 0 &&
|
||||
len(resp.MissingRequiredAttributes) == 0
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// bucketSplit carries the up-to-six entries a single spec bucket contributes
|
||||
// to an onboarding response. Any field may be nil if the bucket doesn't
|
||||
// populate that dimension.
|
||||
type bucketSplit struct {
|
||||
PresentDefault *inframonitoringtypes.MetricsComponentEntry
|
||||
PresentOptional *inframonitoringtypes.MetricsComponentEntry
|
||||
PresentAttrs *inframonitoringtypes.AttributesComponentEntry
|
||||
MissingDefault *inframonitoringtypes.MissingMetricsComponentEntry
|
||||
MissingOptional *inframonitoringtypes.MissingMetricsComponentEntry
|
||||
MissingAttrs *inframonitoringtypes.MissingAttributesComponentEntry
|
||||
}
|
||||
|
||||
// splitBucket partitions one component bucket's metric and attribute lists
|
||||
// against the module-wide missing/present sets into up to six response entries.
|
||||
// Empty partitions are left nil so callers can skip them.
|
||||
func splitBucket(b onboardingComponentBucket, missingMetrics, presentAttrs map[string]bool) bucketSplit {
|
||||
var s bucketSplit
|
||||
|
||||
presentDef, missDef := partitionMetrics(b.DefaultMetrics, missingMetrics)
|
||||
if len(presentDef) > 0 {
|
||||
s.PresentDefault = &inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: presentDef,
|
||||
AssociatedComponent: b.Component,
|
||||
}
|
||||
}
|
||||
if len(missDef) > 0 {
|
||||
s.MissingDefault = &inframonitoringtypes.MissingMetricsComponentEntry{
|
||||
MetricsComponentEntry: inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: missDef,
|
||||
AssociatedComponent: b.Component,
|
||||
},
|
||||
Message: buildMissingDefaultMetricsMessage(missDef, b.Component.Name),
|
||||
DocumentationLink: b.DocumentationLink,
|
||||
}
|
||||
}
|
||||
|
||||
presentOpt, missOpt := partitionMetrics(b.OptionalMetrics, missingMetrics)
|
||||
if len(presentOpt) > 0 {
|
||||
s.PresentOptional = &inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: presentOpt,
|
||||
AssociatedComponent: b.Component,
|
||||
}
|
||||
}
|
||||
if len(missOpt) > 0 {
|
||||
s.MissingOptional = &inframonitoringtypes.MissingMetricsComponentEntry{
|
||||
MetricsComponentEntry: inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: missOpt,
|
||||
AssociatedComponent: b.Component,
|
||||
},
|
||||
Message: buildMissingOptionalMetricsMessage(missOpt, b.Component.Name),
|
||||
DocumentationLink: b.DocumentationLink,
|
||||
}
|
||||
}
|
||||
|
||||
presentA, missA := partitionAttrs(b.RequiredAttrs, presentAttrs)
|
||||
if len(presentA) > 0 {
|
||||
s.PresentAttrs = &inframonitoringtypes.AttributesComponentEntry{
|
||||
Attributes: presentA,
|
||||
AssociatedComponent: b.Component,
|
||||
}
|
||||
}
|
||||
if len(missA) > 0 {
|
||||
s.MissingAttrs = &inframonitoringtypes.MissingAttributesComponentEntry{
|
||||
AttributesComponentEntry: inframonitoringtypes.AttributesComponentEntry{
|
||||
Attributes: missA,
|
||||
AssociatedComponent: b.Component,
|
||||
},
|
||||
Message: buildMissingRequiredAttrsMessage(missA, b.Component.Name),
|
||||
DocumentationLink: b.DocumentationLink,
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// collectSpecUnions returns the de-duplicated unions of (default + optional)
|
||||
// metrics and required attrs across every bucket in a spec. Input order is
|
||||
// preserved (bucket order × within-bucket order) so downstream queries are
|
||||
// deterministic.
|
||||
func collectSpecUnions(spec onboardingSpec) (metrics, attrs []string) {
|
||||
seenMetrics := make(map[string]bool)
|
||||
seenAttrs := make(map[string]bool)
|
||||
for _, b := range spec.Buckets {
|
||||
for _, m := range b.DefaultMetrics {
|
||||
if !seenMetrics[m] {
|
||||
seenMetrics[m] = true
|
||||
metrics = append(metrics, m)
|
||||
}
|
||||
}
|
||||
for _, m := range b.OptionalMetrics {
|
||||
if !seenMetrics[m] {
|
||||
seenMetrics[m] = true
|
||||
metrics = append(metrics, m)
|
||||
}
|
||||
}
|
||||
for _, a := range b.RequiredAttrs {
|
||||
if !seenAttrs[a] {
|
||||
seenAttrs[a] = true
|
||||
attrs = append(attrs, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
return metrics, attrs
|
||||
}
|
||||
|
||||
// partitionMetrics splits a metric-name list into those present (not in
|
||||
// missingMetrics) and those missing (in missingMetrics). Preserves input order.
|
||||
func partitionMetrics(metrics []string, missingMetrics map[string]bool) (present, missing []string) {
|
||||
for _, m := range metrics {
|
||||
if missingMetrics[m] {
|
||||
missing = append(missing, m)
|
||||
} else {
|
||||
present = append(present, m)
|
||||
}
|
||||
}
|
||||
return present, missing
|
||||
}
|
||||
|
||||
// partitionAttrs splits a required-attrs list into present and missing based
|
||||
// on the presentAttrs set returned by getAttributesPresence. Preserves input order.
|
||||
func partitionAttrs(attrs []string, presentAttrs map[string]bool) (present, missing []string) {
|
||||
for _, a := range attrs {
|
||||
if presentAttrs[a] {
|
||||
present = append(present, a)
|
||||
} else {
|
||||
missing = append(missing, a)
|
||||
}
|
||||
}
|
||||
return present, missing
|
||||
}
|
||||
|
||||
func buildMissingDefaultMetricsMessage(metrics []string, componentName string) string {
|
||||
return fmt.Sprintf(
|
||||
"Missing default metrics %s from %s. Learn how to configure here.",
|
||||
strings.Join(metrics, ", "), componentName,
|
||||
)
|
||||
}
|
||||
|
||||
func buildMissingOptionalMetricsMessage(metrics []string, componentName string) string {
|
||||
return fmt.Sprintf(
|
||||
"Missing optional metrics %s from %s. Learn how to enable here.",
|
||||
strings.Join(metrics, ", "), componentName,
|
||||
)
|
||||
}
|
||||
|
||||
func buildMissingRequiredAttrsMessage(attrs []string, componentName string) string {
|
||||
noun := "attribute"
|
||||
if len(attrs) > 1 {
|
||||
noun = "attributes"
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"Missing required %s %s from %s. Learn how to configure here.",
|
||||
noun, strings.Join(attrs, ", "), componentName,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package implinframonitoring
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
|
||||
// Component names — the 5 OTel collector receivers/processors that produce
|
||||
// metrics and resource attributes consumed by infra-monitoring tabs. Bare
|
||||
// strings on purpose (not wrapped enums) — the list is open-ended enough that
|
||||
// an enum adds more friction than value.
|
||||
const (
|
||||
componentNameHostMetricsReceiver = "hostmetricsreceiver"
|
||||
componentNameKubeletStatsReceiver = "kubeletstatsreceiver"
|
||||
componentNameK8sClusterReceiver = "k8sclusterreceiver"
|
||||
componentNameResourceDetectionProcessor = "resourcedetectionprocessor"
|
||||
componentNameK8sAttributesProcessor = "k8sattributesprocessor"
|
||||
)
|
||||
|
||||
// Documentation links — one per component. User-facing; emitted on missing-entries.
|
||||
const (
|
||||
docLinkHostMetricsReceiver = "https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#step-2-configure-the-collector"
|
||||
docLinkKubeletStatsReceiver = "https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/#setting-up-kubelet-stats-monitoring"
|
||||
docLinkK8sClusterReceiver = "https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/#setting-up-k8s-cluster-monitoring"
|
||||
docLinkResourceDetectionProcessor = "https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty"
|
||||
docLinkK8sAttributesProcessor = "https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/#2-enable-kubernetes-metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
componentHostMetricsReceiver = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
|
||||
Name: componentNameHostMetricsReceiver,
|
||||
}
|
||||
componentKubeletStatsReceiver = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
|
||||
Name: componentNameKubeletStatsReceiver,
|
||||
}
|
||||
componentK8sClusterReceiver = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
|
||||
Name: componentNameK8sClusterReceiver,
|
||||
}
|
||||
componentResourceDetectionProcessor = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeProcessor,
|
||||
Name: componentNameResourceDetectionProcessor,
|
||||
}
|
||||
componentK8sAttributesProcessor = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeProcessor,
|
||||
Name: componentNameK8sAttributesProcessor,
|
||||
}
|
||||
)
|
||||
|
||||
// onboardingComponentBucket is a single collector component's contribution
|
||||
// toward a single infra-monitoring tab's readiness. Any of the three dimension
|
||||
// slices (DefaultMetrics, OptionalMetrics, RequiredAttrs) may be empty — the
|
||||
// bucketizer in Phase 4 skips empty dimensions.
|
||||
type onboardingComponentBucket struct {
|
||||
Component inframonitoringtypes.AssociatedComponent
|
||||
DefaultMetrics []string
|
||||
OptionalMetrics []string
|
||||
RequiredAttrs []string
|
||||
DocumentationLink string
|
||||
}
|
||||
|
||||
// onboardingSpec defines, for one OnboardingType, the full set of
|
||||
// component-scoped buckets that must be satisfied for the tab to be ready.
|
||||
type onboardingSpec struct {
|
||||
Buckets []onboardingComponentBucket
|
||||
}
|
||||
|
||||
// Per-type specs. Every metric and attribute is spelled out in its own spec
|
||||
// on purpose — no shared slices, no concatenation helpers. Repetition is
|
||||
// cheaper than indirection when auditing what each tab actually requires.
|
||||
|
||||
var hostsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentHostMetricsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"system.cpu.time",
|
||||
"system.memory.usage",
|
||||
"system.cpu.load_average.15m",
|
||||
"system.filesystem.usage",
|
||||
},
|
||||
DocumentationLink: docLinkHostMetricsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"host.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var processesSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentHostMetricsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"process.cpu.time",
|
||||
"process.memory.usage",
|
||||
},
|
||||
RequiredAttrs: []string{"process.pid"},
|
||||
DocumentationLink: docLinkHostMetricsReceiver,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var podsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{"k8s.pod.phase"},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.pod.uid"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var nodesSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.cpu.usage",
|
||||
"k8s.node.memory.working_set",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.allocatable_cpu",
|
||||
"k8s.node.allocatable_memory",
|
||||
"k8s.node.condition_ready",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.node.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var deploymentsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.container.restarts",
|
||||
"k8s.deployment.desired",
|
||||
"k8s.deployment.available",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.deployment.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var daemonsetsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.container.restarts",
|
||||
"k8s.daemonset.desired_scheduled_nodes",
|
||||
"k8s.daemonset.current_scheduled_nodes",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.daemonset.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var statefulsetsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.container.restarts",
|
||||
"k8s.statefulset.desired_pods",
|
||||
"k8s.statefulset.current_pods",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.statefulset.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var jobsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.container.restarts",
|
||||
"k8s.job.desired_successful_pods",
|
||||
"k8s.job.active_pods",
|
||||
"k8s.job.failed_pods",
|
||||
"k8s.job.successful_pods",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.job.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var namespacesSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{"k8s.pod.phase"},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var clustersSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.cpu.usage",
|
||||
"k8s.node.memory.working_set",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.allocatable_cpu",
|
||||
"k8s.node.allocatable_memory",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var volumesSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.volume.available",
|
||||
"k8s.volume.capacity",
|
||||
"k8s.volume.inodes",
|
||||
"k8s.volume.inodes.free",
|
||||
"k8s.volume.inodes.used",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.persistentvolumeclaim.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// onboardingSpecs is the single lookup table the module consults for a type's
|
||||
// readiness contract. Every OnboardingType value must have an entry here.
|
||||
var onboardingSpecs = map[inframonitoringtypes.OnboardingType]onboardingSpec{
|
||||
inframonitoringtypes.OnboardingTypeHosts: hostsSpec,
|
||||
inframonitoringtypes.OnboardingTypeProcesses: processesSpec,
|
||||
inframonitoringtypes.OnboardingTypePods: podsSpec,
|
||||
inframonitoringtypes.OnboardingTypeNodes: nodesSpec,
|
||||
inframonitoringtypes.OnboardingTypeDeployments: deploymentsSpec,
|
||||
inframonitoringtypes.OnboardingTypeDaemonsets: daemonsetsSpec,
|
||||
inframonitoringtypes.OnboardingTypeStatefulsets: statefulsetsSpec,
|
||||
inframonitoringtypes.OnboardingTypeJobs: jobsSpec,
|
||||
inframonitoringtypes.OnboardingTypeNamespaces: namespacesSpec,
|
||||
inframonitoringtypes.OnboardingTypeClusters: clustersSpec,
|
||||
inframonitoringtypes.OnboardingTypeVolumes: volumesSpec,
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Component used across splitBucket cases — it's a processor so the test
|
||||
// doesn't carry any receiver semantics.
|
||||
var testComponent = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
|
||||
Name: "testreceiver",
|
||||
}
|
||||
|
||||
const testDocLink = "https://example.com/docs"
|
||||
|
||||
func TestSplitBucket(t *testing.T) {
|
||||
type want struct {
|
||||
presentDefault []string
|
||||
presentOptional []string
|
||||
presentAttrs []string
|
||||
missingDefault []string
|
||||
missingOptional []string
|
||||
missingAttrs []string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
bucket onboardingComponentBucket
|
||||
missingMetrics map[string]bool
|
||||
presentAttrs map[string]bool
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "empty bucket — nothing to emit",
|
||||
bucket: onboardingComponentBucket{Component: testComponent, DocumentationLink: testDocLink},
|
||||
missingMetrics: map[string]bool{},
|
||||
presentAttrs: map[string]bool{},
|
||||
want: want{},
|
||||
},
|
||||
{
|
||||
name: "all default metrics present",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
presentAttrs: map[string]bool{},
|
||||
want: want{
|
||||
presentDefault: []string{"m1", "m2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all default metrics missing",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"m1": true, "m2": true},
|
||||
presentAttrs: map[string]bool{},
|
||||
want: want{
|
||||
missingDefault: []string{"m1", "m2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed default metrics",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2", "m3"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"m2": true},
|
||||
presentAttrs: map[string]bool{},
|
||||
want: want{
|
||||
presentDefault: []string{"m1", "m3"},
|
||||
missingDefault: []string{"m2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only optional metrics — all missing",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
OptionalMetrics: []string{"opt1", "opt2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"opt1": true, "opt2": true},
|
||||
presentAttrs: map[string]bool{},
|
||||
want: want{
|
||||
missingOptional: []string{"opt1", "opt2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only required attrs — all present",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
RequiredAttrs: []string{"a1", "a2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
presentAttrs: map[string]bool{"a1": true, "a2": true},
|
||||
want: want{
|
||||
presentAttrs: []string{"a1", "a2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only required attrs — all missing",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
RequiredAttrs: []string{"a1"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
presentAttrs: map[string]bool{},
|
||||
want: want{
|
||||
missingAttrs: []string{"a1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "every dimension populated on both sides",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"d1", "d2"},
|
||||
OptionalMetrics: []string{"o1", "o2"},
|
||||
RequiredAttrs: []string{"a1", "a2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"d2": true, "o1": true},
|
||||
presentAttrs: map[string]bool{"a1": true},
|
||||
want: want{
|
||||
presentDefault: []string{"d1"},
|
||||
missingDefault: []string{"d2"},
|
||||
presentOptional: []string{"o2"},
|
||||
missingOptional: []string{"o1"},
|
||||
presentAttrs: []string{"a1"},
|
||||
missingAttrs: []string{"a2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := splitBucket(tt.bucket, tt.missingMetrics, tt.presentAttrs)
|
||||
|
||||
requireMetricsEntry(t, "presentDefault", got.PresentDefault, tt.want.presentDefault, false)
|
||||
requireMetricsEntry(t, "presentOptional", got.PresentOptional, tt.want.presentOptional, false)
|
||||
requireAttrsEntry(t, "presentAttrs", got.PresentAttrs, tt.want.presentAttrs, false)
|
||||
|
||||
requireMissingMetrics(t, "missingDefault", got.MissingDefault, tt.want.missingDefault)
|
||||
requireMissingMetrics(t, "missingOptional", got.MissingOptional, tt.want.missingOptional)
|
||||
requireMissingAttrs(t, "missingAttrs", got.MissingAttrs, tt.want.missingAttrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectSpecUnions_Dedupes(t *testing.T) {
|
||||
spec := onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2"},
|
||||
OptionalMetrics: []string{"m1"}, // dup across dimensions
|
||||
RequiredAttrs: []string{"a1"},
|
||||
},
|
||||
{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m2", "m3"}, // dup across buckets
|
||||
RequiredAttrs: []string{"a1", "a2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
metrics, attrs := collectSpecUnions(spec)
|
||||
require.Equal(t, []string{"m1", "m2", "m3"}, metrics)
|
||||
require.Equal(t, []string{"a1", "a2"}, attrs)
|
||||
}
|
||||
|
||||
func TestPartitionMetrics(t *testing.T) {
|
||||
present, missing := partitionMetrics(
|
||||
[]string{"a", "b", "c", "d"},
|
||||
map[string]bool{"b": true, "d": true},
|
||||
)
|
||||
require.Equal(t, []string{"a", "c"}, present)
|
||||
require.Equal(t, []string{"b", "d"}, missing)
|
||||
}
|
||||
|
||||
func TestPartitionAttrs(t *testing.T) {
|
||||
present, missing := partitionAttrs(
|
||||
[]string{"a", "b", "c"},
|
||||
map[string]bool{"a": true, "c": true},
|
||||
)
|
||||
require.Equal(t, []string{"a", "c"}, present)
|
||||
require.Equal(t, []string{"b"}, missing)
|
||||
}
|
||||
|
||||
func TestMissingMessageTemplates(t *testing.T) {
|
||||
require.Equal(t,
|
||||
"Missing default metrics m1, m2 from comp. Learn how to configure here.",
|
||||
buildMissingDefaultMetricsMessage([]string{"m1", "m2"}, "comp"),
|
||||
)
|
||||
require.Equal(t,
|
||||
"Missing optional metrics m1 from comp. Learn how to enable here.",
|
||||
buildMissingOptionalMetricsMessage([]string{"m1"}, "comp"),
|
||||
)
|
||||
require.Equal(t,
|
||||
"Missing required attribute a1 from comp. Learn how to configure here.",
|
||||
buildMissingRequiredAttrsMessage([]string{"a1"}, "comp"),
|
||||
)
|
||||
require.Equal(t,
|
||||
"Missing required attributes a1, a2 from comp. Learn how to configure here.",
|
||||
buildMissingRequiredAttrsMessage([]string{"a1", "a2"}, "comp"),
|
||||
)
|
||||
}
|
||||
|
||||
// TestOnboardingSpecs_CoverAllTypes ensures the spec map has an entry for
|
||||
// every OnboardingType — prevents silently shipping an onboarding type that
|
||||
// has no spec and would 500 at runtime.
|
||||
func TestOnboardingSpecs_CoverAllTypes(t *testing.T) {
|
||||
for _, tp := range inframonitoringtypes.ValidOnboardingTypes {
|
||||
_, ok := onboardingSpecs[tp]
|
||||
require.True(t, ok, "missing onboarding spec for type %s", tp)
|
||||
}
|
||||
require.Len(t, onboardingSpecs, len(inframonitoringtypes.ValidOnboardingTypes))
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func requireMetricsEntry(t *testing.T, name string, got *inframonitoringtypes.MetricsComponentEntry, wantMetrics []string, _ bool) {
|
||||
t.Helper()
|
||||
if len(wantMetrics) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantMetrics, got.Metrics, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
}
|
||||
|
||||
func requireAttrsEntry(t *testing.T, name string, got *inframonitoringtypes.AttributesComponentEntry, wantAttrs []string, _ bool) {
|
||||
t.Helper()
|
||||
if len(wantAttrs) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantAttrs, got.Attributes, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
}
|
||||
|
||||
func requireMissingMetrics(t *testing.T, name string, got *inframonitoringtypes.MissingMetricsComponentEntry, wantMetrics []string) {
|
||||
t.Helper()
|
||||
if len(wantMetrics) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantMetrics, got.Metrics, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
require.NotEmpty(t, got.Message, name)
|
||||
require.Equal(t, testDocLink, got.DocumentationLink, name)
|
||||
}
|
||||
|
||||
func requireMissingAttrs(t *testing.T, name string, got *inframonitoringtypes.MissingAttributesComponentEntry, wantAttrs []string) {
|
||||
t.Helper()
|
||||
if len(wantAttrs) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantAttrs, got.Attributes, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
require.NotEmpty(t, got.Message, name)
|
||||
require.Equal(t, testDocLink, got.DocumentationLink, name)
|
||||
}
|
||||
154
pkg/modules/inframonitoring/implinframonitoring/pods.go
Normal file
154
pkg/modules/inframonitoring/implinframonitoring/pods.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func mapPhaseNumToString(v float64) inframonitoringtypes.PodPhase {
|
||||
switch int(v) {
|
||||
case 1:
|
||||
return inframonitoringtypes.PodPhasePending
|
||||
case 2:
|
||||
return inframonitoringtypes.PodPhaseRunning
|
||||
case 3:
|
||||
return inframonitoringtypes.PodPhaseSucceeded
|
||||
case 4:
|
||||
return inframonitoringtypes.PodPhaseFailed
|
||||
default:
|
||||
return inframonitoringtypes.PodPhaseNone
|
||||
}
|
||||
}
|
||||
|
||||
func (m *module) getTopPodGroups(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostablePods,
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
queryNamesForOrderBy := orderByToPodsQueryNames[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.newPodsTableListQuery().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) getPodsTableMetadata(ctx context.Context, req *inframonitoringtypes.PostablePods) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range podAttrKeysForMetadata {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, podsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
func buildPodRecords(
|
||||
resp *qbtypes.QueryRangeResponse,
|
||||
pageGroups []map[string]string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
metadataMap map[string]map[string]string,
|
||||
reqEnd int64,
|
||||
) []inframonitoringtypes.PodRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
records := make([]inframonitoringtypes.PodRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
podUID := labels[podUIDAttrKey]
|
||||
|
||||
record := inframonitoringtypes.PodRecord{
|
||||
PodUID: podUID,
|
||||
PodCPU: -1,
|
||||
PodCPURequest: -1,
|
||||
PodCPULimit: -1,
|
||||
PodMemory: -1,
|
||||
PodMemoryRequest: -1,
|
||||
PodMemoryLimit: -1,
|
||||
PodAge: -1,
|
||||
Meta: map[string]any{},
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
if v, exists := metrics["A"]; exists {
|
||||
record.PodCPU = v
|
||||
}
|
||||
if v, exists := metrics["B"]; exists {
|
||||
record.PodCPURequest = v
|
||||
}
|
||||
if v, exists := metrics["C"]; exists {
|
||||
record.PodCPULimit = v
|
||||
}
|
||||
if v, exists := metrics["D"]; exists {
|
||||
record.PodMemory = v
|
||||
}
|
||||
if v, exists := metrics["E"]; exists {
|
||||
record.PodMemoryRequest = v
|
||||
}
|
||||
if v, exists := metrics["F"]; exists {
|
||||
record.PodMemoryLimit = v
|
||||
}
|
||||
if v, exists := metrics["G"]; exists {
|
||||
record.PodPhase = mapPhaseNumToString(v)
|
||||
}
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
if startTimeStr, exists := attrs[podStartTimeAttrKey]; exists && startTimeStr != "" {
|
||||
if t, err := time.Parse(time.RFC3339, startTimeStr); err == nil {
|
||||
startTimeMs := t.UnixMilli()
|
||||
if startTimeMs > 0 {
|
||||
record.PodAge = reqEnd - startTimeMs
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
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 (
|
||||
podUIDAttrKey = "k8s.pod.uid"
|
||||
podStartTimeAttrKey = "k8s.pod.start_time"
|
||||
)
|
||||
|
||||
var podUIDGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: podUIDAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
}
|
||||
|
||||
var podsTableMetricNamesList = []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory.working_set",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
"k8s.pod.phase",
|
||||
}
|
||||
|
||||
var podAttrKeysForMetadata = []string{
|
||||
"k8s.pod.uid",
|
||||
"k8s.pod.name",
|
||||
"k8s.namespace.name",
|
||||
"k8s.node.name",
|
||||
"k8s.deployment.name",
|
||||
"k8s.statefulset.name",
|
||||
"k8s.daemonset.name",
|
||||
"k8s.job.name",
|
||||
"k8s.cronjob.name",
|
||||
"k8s.cluster.name",
|
||||
"k8s.pod.start_time",
|
||||
}
|
||||
|
||||
var orderByToPodsQueryNames = map[string][]string{
|
||||
inframonitoringtypes.PodsOrderByCPU: {"A"},
|
||||
inframonitoringtypes.PodsOrderByCPURequest: {"B"},
|
||||
inframonitoringtypes.PodsOrderByCPULimit: {"C"},
|
||||
inframonitoringtypes.PodsOrderByMemory: {"D"},
|
||||
inframonitoringtypes.PodsOrderByMemoryRequest: {"E"},
|
||||
inframonitoringtypes.PodsOrderByMemoryLimit: {"F"},
|
||||
inframonitoringtypes.PodsOrderByPhase: {"G"},
|
||||
}
|
||||
|
||||
func (m *module) newPodsTableListQuery() *qbtypes.QueryRangeRequest {
|
||||
return &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
// Query A: CPU usage
|
||||
{
|
||||
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{podUIDGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query B: CPU request utilization
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.cpu_request_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{podUIDGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query C: CPU limit utilization
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "C",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.cpu_limit_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{podUIDGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query D: Memory working set
|
||||
{
|
||||
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{podUIDGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query E: Memory request utilization
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "E",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.memory_request_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{podUIDGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query F: Memory limit utilization
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "F",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.memory_limit_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{podUIDGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query G: Pod phase (latest value per pod)
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "G",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.phase",
|
||||
TimeAggregation: metrictypes.TimeAggregationLatest,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationMax,
|
||||
ReduceTo: qbtypes.ReduceToLast,
|
||||
},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{podUIDGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
21
pkg/modules/inframonitoring/inframonitoring.go
Normal file
21
pkg/modules/inframonitoring/inframonitoring.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package inframonitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
ListHosts(http.ResponseWriter, *http.Request)
|
||||
ListPods(http.ResponseWriter, *http.Request)
|
||||
GetOnboarding(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)
|
||||
GetOnboarding(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableOnboarding) (*inframonitoringtypes.Onboarding, error)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/identn"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
@@ -114,6 +115,9 @@ type Config struct {
|
||||
// MetricsExplorer config
|
||||
MetricsExplorer metricsexplorer.Config `mapstructure:"metricsexplorer"`
|
||||
|
||||
// InfraMonitoring config
|
||||
InfraMonitoring inframonitoring.Config `mapstructure:"inframonitoring"`
|
||||
|
||||
// Flagger config
|
||||
Flagger flagger.Config `mapstructure:"flagger"`
|
||||
|
||||
@@ -157,6 +161,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
|
||||
gateway.NewConfigFactory(),
|
||||
tokenizer.NewConfigFactory(),
|
||||
metricsexplorer.NewConfigFactory(),
|
||||
inframonitoring.NewConfigFactory(),
|
||||
flagger.NewConfigFactory(),
|
||||
user.NewConfigFactory(),
|
||||
identn.NewConfigFactory(),
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields/implfields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
@@ -55,6 +57,7 @@ type Handlers struct {
|
||||
SpanPercentile spanpercentile.Handler
|
||||
Services services.Handler
|
||||
MetricsExplorer metricsexplorer.Handler
|
||||
InfraMonitoring inframonitoring.Handler
|
||||
Global global.Handler
|
||||
FlaggerHandler flagger.Handler
|
||||
GatewayHandler gateway.Handler
|
||||
@@ -95,6 +98,7 @@ func NewHandlers(
|
||||
RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport),
|
||||
Services: implservices.NewHandler(modules.Services),
|
||||
MetricsExplorer: implmetricsexplorer.NewHandler(modules.MetricsExplorer),
|
||||
InfraMonitoring: implinframonitoring.NewHandler(modules.InfraMonitoring),
|
||||
SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile),
|
||||
Global: signozglobal.NewHandler(global),
|
||||
FlaggerHandler: flagger.NewHandler(flaggerService),
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
@@ -69,6 +71,7 @@ type Modules struct {
|
||||
Services services.Module
|
||||
SpanPercentile spanpercentile.Module
|
||||
MetricsExplorer metricsexplorer.Module
|
||||
InfraMonitoring inframonitoring.Module
|
||||
Promote promote.Module
|
||||
ServiceAccount serviceaccount.Module
|
||||
CloudIntegration cloudintegration.Module
|
||||
@@ -119,6 +122,7 @@ func NewModules(
|
||||
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||
Services: implservices.NewModule(querier, telemetryStore),
|
||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
|
||||
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, providerSettings, config.InfraMonitoring),
|
||||
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
||||
ServiceAccount: serviceAccount,
|
||||
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger)),
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/promote"
|
||||
@@ -61,6 +62,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ dashboard.Module }{},
|
||||
struct{ dashboard.Handler }{},
|
||||
struct{ metricsexplorer.Handler }{},
|
||||
struct{ inframonitoring.Handler }{},
|
||||
struct{ gateway.Handler }{},
|
||||
struct{ fields.Handler }{},
|
||||
struct{ authz.Handler }{},
|
||||
|
||||
@@ -272,6 +272,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
modules.Dashboard,
|
||||
handlers.Dashboard,
|
||||
handlers.MetricsExplorer,
|
||||
handlers.InfraMonitoring,
|
||||
handlers.GatewayHandler,
|
||||
handlers.Fields,
|
||||
handlers.AuthzHandler,
|
||||
|
||||
21
pkg/types/inframonitoringtypes/commontypes.go
Normal file
21
pkg/types/inframonitoringtypes/commontypes.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type ResponseType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
ResponseTypeList = ResponseType{valuer.NewString("list")}
|
||||
ResponseTypeGroupedList = ResponseType{valuer.NewString("grouped_list")}
|
||||
)
|
||||
|
||||
func (ResponseType) Enum() []any {
|
||||
return []any{
|
||||
ResponseTypeList,
|
||||
ResponseTypeGroupedList,
|
||||
}
|
||||
}
|
||||
117
pkg/types/inframonitoringtypes/hosts.go
Normal file
117
pkg/types/inframonitoringtypes/hosts.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
type Hosts struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []HostRecord `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 HostRecord struct {
|
||||
HostName string `json:"hostName" required:"true"`
|
||||
Status HostStatus `json:"status" required:"true"`
|
||||
ActiveHostCount int `json:"activeHostCount" required:"true"`
|
||||
InactiveHostCount int `json:"inactiveHostCount" required:"true"`
|
||||
CPU float64 `json:"cpu" required:"true"`
|
||||
Memory float64 `json:"memory" required:"true"`
|
||||
Wait float64 `json:"wait" required:"true"`
|
||||
Load15 float64 `json:"load15" required:"true"`
|
||||
DiskUsage float64 `json:"diskUsage" required:"true"`
|
||||
Meta map[string]interface{} `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
type RequiredMetricsCheck struct {
|
||||
MissingMetrics []string `json:"missingMetrics" required:"true"`
|
||||
}
|
||||
|
||||
type PostableHosts struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *HostFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
type HostFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByStatus HostStatus `json:"filterByStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures HostsListRequest contains acceptable values.
|
||||
func (req *PostableHosts) 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.Filter != nil && !req.Filter.FilterByStatus.IsZero() &&
|
||||
req.Filter.FilterByStatus != HostStatusActive && req.Filter.FilterByStatus != HostStatusInactive {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by status: %s", req.Filter.FilterByStatus)
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(HostsValidOrderByKeys, 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 *PostableHosts) UnmarshalJSON(data []byte) error {
|
||||
type raw PostableHosts
|
||||
var decoded raw
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
*req = PostableHosts(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
37
pkg/types/inframonitoringtypes/hosts_constants.go
Normal file
37
pkg/types/inframonitoringtypes/hosts_constants.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
type HostStatus struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
HostStatusActive = HostStatus{valuer.NewString("active")}
|
||||
HostStatusInactive = HostStatus{valuer.NewString("inactive")}
|
||||
HostStatusNone = HostStatus{valuer.NewString("")}
|
||||
)
|
||||
|
||||
func (HostStatus) Enum() []any {
|
||||
return []any{
|
||||
HostStatusActive,
|
||||
HostStatusInactive,
|
||||
HostStatusNone,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
HostsOrderByCPU = "cpu"
|
||||
HostsOrderByMemory = "memory"
|
||||
HostsOrderByWait = "wait"
|
||||
HostsOrderByDiskUsage = "disk_usage"
|
||||
HostsOrderByLoad15 = "load15"
|
||||
)
|
||||
|
||||
var HostsValidOrderByKeys = []string{
|
||||
HostsOrderByCPU,
|
||||
HostsOrderByMemory,
|
||||
HostsOrderByWait,
|
||||
HostsOrderByDiskUsage,
|
||||
HostsOrderByLoad15,
|
||||
}
|
||||
244
pkg/types/inframonitoringtypes/hosts_test.go
Normal file
244
pkg/types/inframonitoringtypes/hosts_test.go
Normal file
@@ -0,0 +1,244 @@
|
||||
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 TestHostsListRequest_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *PostableHosts
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid request",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil request",
|
||||
req: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time zero",
|
||||
req: &PostableHosts{
|
||||
Start: 0,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time negative",
|
||||
req: &PostableHosts{
|
||||
Start: -1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "end time zero",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 0,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time greater than end time",
|
||||
req: &PostableHosts{
|
||||
Start: 2000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time equal to end time",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit zero",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 0,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit negative",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: -10,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit exceeds max",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 5001,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "offset negative",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: -5,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "filter by status ACTIVE",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
Filter: &HostFilter{FilterByStatus: HostStatusActive},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter by status INACTIVE",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
Filter: &HostFilter{FilterByStatus: HostStatusInactive},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter by status empty (zero value)",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter by status invalid value",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
Filter: &HostFilter{FilterByStatus: HostStatus{valuer.NewString("UNKNOWN")}},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy nil is valid",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key cpu and direction asc",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: HostsOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with invalid key",
|
||||
req: &PostableHosts{
|
||||
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: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: HostsOrderByMemory,
|
||||
},
|
||||
},
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
83
pkg/types/inframonitoringtypes/onboarding.go
Normal file
83
pkg/types/inframonitoringtypes/onboarding.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// PostableOnboarding is the request for GET /api/v2/infra_monitoring/onboarding.
|
||||
// The single `type` query param selects which infra-monitoring subsection the
|
||||
// readiness check runs for.
|
||||
type PostableOnboarding struct {
|
||||
Type OnboardingType `query:"type" required:"true"`
|
||||
}
|
||||
|
||||
// Validate rejects empty/unknown onboarding types.
|
||||
func (req *PostableOnboarding) Validate() error {
|
||||
if req == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
|
||||
if req.Type.IsZero() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "type is required")
|
||||
}
|
||||
|
||||
if !slices.Contains(ValidOnboardingTypes, req.Type) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid type: %s", req.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Onboarding is the response for GET /api/v2/infra_monitoring/onboarding.
|
||||
//
|
||||
// The three present/missing pairs partition a type's requirements into three
|
||||
// dimensions — default-enabled metrics, optional metrics, required attributes —
|
||||
// each bucketed by the collector component (receiver or processor) that
|
||||
// produces it. Ready is true iff every Missing* array is empty.
|
||||
type Onboarding struct {
|
||||
Type OnboardingType `json:"type" required:"true"`
|
||||
Ready bool `json:"ready" required:"true"`
|
||||
PresentDefaultEnabledMetrics []MetricsComponentEntry `json:"presentDefaultEnabledMetrics" required:"true"`
|
||||
PresentOptionalMetrics []MetricsComponentEntry `json:"presentOptionalMetrics" required:"true"`
|
||||
PresentRequiredAttributes []AttributesComponentEntry `json:"presentRequiredAttributes" required:"true"`
|
||||
MissingDefaultEnabledMetrics []MissingMetricsComponentEntry `json:"missingDefaultEnabledMetrics" required:"true"`
|
||||
MissingOptionalMetrics []MissingMetricsComponentEntry `json:"missingOptionalMetrics" required:"true"`
|
||||
MissingRequiredAttributes []MissingAttributesComponentEntry `json:"missingRequiredAttributes" required:"true"`
|
||||
}
|
||||
|
||||
// AssociatedComponent identifies the collector receiver or processor that a
|
||||
// metric or attribute originates from. Name is free-form (e.g. "kubeletstatsreceiver").
|
||||
type AssociatedComponent struct {
|
||||
Type OnboardingComponentType `json:"type" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
}
|
||||
|
||||
// MetricsComponentEntry lists metrics that share a single associated component.
|
||||
type MetricsComponentEntry struct {
|
||||
Metrics []string `json:"metrics" required:"true"`
|
||||
AssociatedComponent AssociatedComponent `json:"associatedComponent" required:"true"`
|
||||
}
|
||||
|
||||
// AttributesComponentEntry lists resource attributes that share a single associated component.
|
||||
type AttributesComponentEntry struct {
|
||||
Attributes []string `json:"attributes" required:"true"`
|
||||
AssociatedComponent AssociatedComponent `json:"associatedComponent" required:"true"`
|
||||
}
|
||||
|
||||
// MissingMetricsComponentEntry extends MetricsComponentEntry with a user-facing
|
||||
// message and a docs link for fixing the missing metrics.
|
||||
type MissingMetricsComponentEntry struct {
|
||||
MetricsComponentEntry
|
||||
Message string `json:"message" required:"true"`
|
||||
DocumentationLink string `json:"documentationLink" required:"true"`
|
||||
}
|
||||
|
||||
// MissingAttributesComponentEntry extends AttributesComponentEntry with a user-facing
|
||||
// message and a docs link for fixing the missing attributes.
|
||||
type MissingAttributesComponentEntry struct {
|
||||
AttributesComponentEntry
|
||||
Message string `json:"message" required:"true"`
|
||||
DocumentationLink string `json:"documentationLink" required:"true"`
|
||||
}
|
||||
71
pkg/types/inframonitoringtypes/onboarding_constants.go
Normal file
71
pkg/types/inframonitoringtypes/onboarding_constants.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
// OnboardingType identifies a single infra-monitoring subsection (UI tab).
|
||||
// One value per v1/v2 list API we surface in the infra-monitoring section.
|
||||
type OnboardingType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
OnboardingTypeHosts = OnboardingType{valuer.NewString("hosts")}
|
||||
OnboardingTypeProcesses = OnboardingType{valuer.NewString("processes")}
|
||||
OnboardingTypePods = OnboardingType{valuer.NewString("pods")}
|
||||
OnboardingTypeNodes = OnboardingType{valuer.NewString("nodes")}
|
||||
OnboardingTypeDeployments = OnboardingType{valuer.NewString("deployments")}
|
||||
OnboardingTypeDaemonsets = OnboardingType{valuer.NewString("daemonsets")}
|
||||
OnboardingTypeStatefulsets = OnboardingType{valuer.NewString("statefulsets")}
|
||||
OnboardingTypeJobs = OnboardingType{valuer.NewString("jobs")}
|
||||
OnboardingTypeNamespaces = OnboardingType{valuer.NewString("namespaces")}
|
||||
OnboardingTypeClusters = OnboardingType{valuer.NewString("clusters")}
|
||||
OnboardingTypeVolumes = OnboardingType{valuer.NewString("volumes")}
|
||||
)
|
||||
|
||||
func (OnboardingType) Enum() []any {
|
||||
return []any{
|
||||
OnboardingTypeHosts,
|
||||
OnboardingTypeProcesses,
|
||||
OnboardingTypePods,
|
||||
OnboardingTypeNodes,
|
||||
OnboardingTypeDeployments,
|
||||
OnboardingTypeDaemonsets,
|
||||
OnboardingTypeStatefulsets,
|
||||
OnboardingTypeJobs,
|
||||
OnboardingTypeNamespaces,
|
||||
OnboardingTypeClusters,
|
||||
OnboardingTypeVolumes,
|
||||
}
|
||||
}
|
||||
|
||||
var ValidOnboardingTypes = []OnboardingType{
|
||||
OnboardingTypeHosts,
|
||||
OnboardingTypeProcesses,
|
||||
OnboardingTypePods,
|
||||
OnboardingTypeNodes,
|
||||
OnboardingTypeDeployments,
|
||||
OnboardingTypeDaemonsets,
|
||||
OnboardingTypeStatefulsets,
|
||||
OnboardingTypeJobs,
|
||||
OnboardingTypeNamespaces,
|
||||
OnboardingTypeClusters,
|
||||
OnboardingTypeVolumes,
|
||||
}
|
||||
|
||||
// OnboardingComponentType tags each AssociatedComponent as either a receiver or a processor.
|
||||
// Only these two values are ever written by the module.
|
||||
type OnboardingComponentType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
OnboardingComponentTypeReceiver = OnboardingComponentType{valuer.NewString("receiver")}
|
||||
OnboardingComponentTypeProcessor = OnboardingComponentType{valuer.NewString("processor")}
|
||||
)
|
||||
|
||||
func (OnboardingComponentType) Enum() []any {
|
||||
return []any{
|
||||
OnboardingComponentTypeReceiver,
|
||||
OnboardingComponentTypeProcessor,
|
||||
}
|
||||
}
|
||||
110
pkg/types/inframonitoringtypes/onboarding_test.go
Normal file
110
pkg/types/inframonitoringtypes/onboarding_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPostableOnboarding_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *PostableOnboarding
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
req: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty type",
|
||||
req: &PostableOnboarding{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown type",
|
||||
req: &PostableOnboarding{Type: OnboardingType{valuer.NewString("foo")}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "hosts",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeHosts},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "processes",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeProcesses},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "pods",
|
||||
req: &PostableOnboarding{Type: OnboardingTypePods},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nodes",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeNodes},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "deployments",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeDeployments},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "daemonsets",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeDaemonsets},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "statefulsets",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeStatefulsets},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "jobs",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeJobs},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "namespaces",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeNamespaces},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "clusters",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeClusters},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "volumes",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeVolumes},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidOnboardingTypes_MatchesEnum ensures the ValidOnboardingTypes slice
|
||||
// stays in sync with the Enum() list — both must cover every OnboardingType value.
|
||||
func TestValidOnboardingTypes_MatchesEnum(t *testing.T) {
|
||||
enum := OnboardingType{}.Enum()
|
||||
require.Equal(t, len(enum), len(ValidOnboardingTypes))
|
||||
for i, v := range enum {
|
||||
require.Equal(t, v, ValidOnboardingTypes[i])
|
||||
}
|
||||
}
|
||||
104
pkg/types/inframonitoringtypes/pods.go
Normal file
104
pkg/types/inframonitoringtypes/pods.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
type Pods struct {
|
||||
Type ResponseType `json:"type"`
|
||||
Records []PodRecord `json:"records"`
|
||||
Total int `json:"total"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
|
||||
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
|
||||
}
|
||||
|
||||
type PodRecord struct {
|
||||
PodUID string `json:"podUID,omitempty"`
|
||||
PodCPU float64 `json:"podCPU"`
|
||||
PodCPURequest float64 `json:"podCPURequest"`
|
||||
PodCPULimit float64 `json:"podCPULimit"`
|
||||
PodMemory float64 `json:"podMemory"`
|
||||
PodMemoryRequest float64 `json:"podMemoryRequest"`
|
||||
PodMemoryLimit float64 `json:"podMemoryLimit"`
|
||||
PodPhase PodPhase `json:"podPhase"`
|
||||
PodAge int64 `json:"podAge"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
}
|
||||
|
||||
// PostablePods is the request body for the v2 pods list API.
|
||||
type PostablePods struct {
|
||||
Start int64 `json:"start"`
|
||||
End int64 `json:"end"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// Validate ensures PostablePods contains acceptable values.
|
||||
func (req *PostablePods) 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(PodsValidOrderByKeys, 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 *PostablePods) UnmarshalJSON(data []byte) error {
|
||||
type raw PostablePods
|
||||
var decoded raw
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
*req = PostablePods(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
45
pkg/types/inframonitoringtypes/pods_constants.go
Normal file
45
pkg/types/inframonitoringtypes/pods_constants.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
type PodPhase struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
PodPhasePending = PodPhase{valuer.NewString("pending")}
|
||||
PodPhaseRunning = PodPhase{valuer.NewString("running")}
|
||||
PodPhaseSucceeded = PodPhase{valuer.NewString("succeeded")}
|
||||
PodPhaseFailed = PodPhase{valuer.NewString("failed")}
|
||||
PodPhaseNone = PodPhase{valuer.NewString("")}
|
||||
)
|
||||
|
||||
func (PodPhase) Enum() []any {
|
||||
return []any{
|
||||
PodPhasePending,
|
||||
PodPhaseRunning,
|
||||
PodPhaseSucceeded,
|
||||
PodPhaseFailed,
|
||||
PodPhaseNone,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
PodsOrderByCPU = "cpu"
|
||||
PodsOrderByCPURequest = "cpu_request"
|
||||
PodsOrderByCPULimit = "cpu_limit"
|
||||
PodsOrderByMemory = "memory"
|
||||
PodsOrderByMemoryRequest = "memory_request"
|
||||
PodsOrderByMemoryLimit = "memory_limit"
|
||||
PodsOrderByPhase = "phase"
|
||||
)
|
||||
|
||||
var PodsValidOrderByKeys = []string{
|
||||
PodsOrderByCPU,
|
||||
PodsOrderByCPURequest,
|
||||
PodsOrderByCPULimit,
|
||||
PodsOrderByMemory,
|
||||
PodsOrderByMemoryRequest,
|
||||
PodsOrderByMemoryLimit,
|
||||
PodsOrderByPhase,
|
||||
}
|
||||
219
pkg/types/inframonitoringtypes/pods_test.go
Normal file
219
pkg/types/inframonitoringtypes/pods_test.go
Normal file
@@ -0,0 +1,219 @@
|
||||
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 TestPostablePods_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *PostablePods
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid request",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil request",
|
||||
req: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time zero",
|
||||
req: &PostablePods{
|
||||
Start: 0,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time negative",
|
||||
req: &PostablePods{
|
||||
Start: -1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "end time zero",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 0,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time greater than end time",
|
||||
req: &PostablePods{
|
||||
Start: 2000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time equal to end time",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit zero",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 0,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit negative",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: -10,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit exceeds max",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 5001,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "offset negative",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: -5,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy nil is valid",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key cpu and direction asc",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: PodsOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key phase and direction desc",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: PodsOrderByPhase,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with invalid key",
|
||||
req: &PostablePods{
|
||||
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: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: PodsOrderByMemory,
|
||||
},
|
||||
},
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user