mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-20 23:30:31 +01:00
Compare commits
137 Commits
main
...
infraM/v2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d147b0177a | ||
|
|
e833952c66 | ||
|
|
c4a46a5d7d | ||
|
|
8931c593d6 | ||
|
|
2d0a6d80f7 | ||
|
|
4543c0008b | ||
|
|
405c7d13ad | ||
|
|
3c1a4b4103 | ||
|
|
b414fc30af | ||
|
|
7dd64c0d53 | ||
|
|
e4a8c581d1 | ||
|
|
a949993430 | ||
|
|
f34a33e08b | ||
|
|
46e833faba | ||
|
|
4bd7492629 | ||
|
|
24fe9a986d | ||
|
|
56e79be6cd | ||
|
|
92d297ac9d | ||
|
|
b3c352609c | ||
|
|
bdbaa32485 | ||
|
|
9503cdff36 | ||
|
|
5a18786ab2 | ||
|
|
648154df14 | ||
|
|
98eb002e07 | ||
|
|
720379db9f | ||
|
|
6ad14e7151 | ||
|
|
181fca064b | ||
|
|
a5e39ca6bd | ||
|
|
b35c6676f9 | ||
|
|
1095caa123 | ||
|
|
9043b49762 | ||
|
|
d4084a7494 | ||
|
|
27c564b3bf | ||
|
|
f02c491828 | ||
|
|
3d53b8f77f | ||
|
|
dffe94fec4 | ||
|
|
c9360fcf13 | ||
|
|
b5ab45db20 | ||
|
|
08f76aca78 | ||
|
|
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 |
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -189,13 +189,6 @@ go.mod @therealpandey
|
||||
/frontend/src/container/TriggeredAlerts/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/AnomalyAlertEvaluationView/ @SigNoz/pulse-frontend
|
||||
|
||||
## Notification Channels
|
||||
/frontend/src/pages/ChannelsEdit/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/ChannelsNew/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/AllAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/CreateAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/EditAlertChannels/ @SigNoz/pulse-frontend
|
||||
|
||||
## OpenAPI Schema - Generated
|
||||
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv
|
||||
/docs/api/openapi.yml @therealpandey @vikrantgupta25 @srikanthccv
|
||||
|
||||
@@ -3895,6 +3895,29 @@ components:
|
||||
enabled:
|
||||
type: boolean
|
||||
type: object
|
||||
InframonitoringtypesAssociatedComponent:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/InframonitoringtypesOnboardingComponentType'
|
||||
required:
|
||||
- type
|
||||
- name
|
||||
type: object
|
||||
InframonitoringtypesAttributesComponentEntry:
|
||||
properties:
|
||||
associatedComponent:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAssociatedComponent'
|
||||
attributes:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- attributes
|
||||
- associatedComponent
|
||||
type: object
|
||||
InframonitoringtypesClusterRecord:
|
||||
properties:
|
||||
clusterCPU:
|
||||
@@ -4244,6 +4267,57 @@ components:
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesMetricsComponentEntry:
|
||||
properties:
|
||||
associatedComponent:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAssociatedComponent'
|
||||
metrics:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- metrics
|
||||
- associatedComponent
|
||||
type: object
|
||||
InframonitoringtypesMissingAttributesComponentEntry:
|
||||
properties:
|
||||
associatedComponent:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAssociatedComponent'
|
||||
attributes:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
documentationLink:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- attributes
|
||||
- associatedComponent
|
||||
- message
|
||||
- documentationLink
|
||||
type: object
|
||||
InframonitoringtypesMissingMetricsComponentEntry:
|
||||
properties:
|
||||
associatedComponent:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAssociatedComponent'
|
||||
documentationLink:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
metrics:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- metrics
|
||||
- associatedComponent
|
||||
- message
|
||||
- documentationLink
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
meta:
|
||||
@@ -4368,6 +4442,71 @@ components:
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesOnboarding:
|
||||
properties:
|
||||
missingDefaultEnabledMetrics:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMissingMetricsComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
missingOptionalMetrics:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMissingMetricsComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
missingRequiredAttributes:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMissingAttributesComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
presentDefaultEnabledMetrics:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMetricsComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
presentOptionalMetrics:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMetricsComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
presentRequiredAttributes:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAttributesComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
ready:
|
||||
type: boolean
|
||||
type:
|
||||
$ref: '#/components/schemas/InframonitoringtypesOnboardingType'
|
||||
required:
|
||||
- type
|
||||
- ready
|
||||
- presentDefaultEnabledMetrics
|
||||
- presentOptionalMetrics
|
||||
- presentRequiredAttributes
|
||||
- missingDefaultEnabledMetrics
|
||||
- missingOptionalMetrics
|
||||
- missingRequiredAttributes
|
||||
type: object
|
||||
InframonitoringtypesOnboardingComponentType:
|
||||
enum:
|
||||
- receiver
|
||||
- processor
|
||||
type: string
|
||||
InframonitoringtypesOnboardingType:
|
||||
enum:
|
||||
- hosts
|
||||
- processes
|
||||
- pods
|
||||
- nodes
|
||||
- deployments
|
||||
- daemonsets
|
||||
- statefulsets
|
||||
- jobs
|
||||
- namespaces
|
||||
- clusters
|
||||
- volumes
|
||||
type: string
|
||||
InframonitoringtypesPodCountsByPhase:
|
||||
properties:
|
||||
failed:
|
||||
@@ -15189,6 +15328,72 @@ paths:
|
||||
summary: List Nodes for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/onboarding:
|
||||
get:
|
||||
deprecated: false
|
||||
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.'
|
||||
operationId: GetOnboarding
|
||||
parameters:
|
||||
- in: query
|
||||
name: type
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/InframonitoringtypesOnboardingType'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/InframonitoringtypesOnboarding'
|
||||
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: Get Onboarding Status for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/pods:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -48,14 +48,13 @@ const config: Config.InitialOptions = {
|
||||
],
|
||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||
},
|
||||
// TODO: https://github.com/SigNoz/engineering-pod/issues/5334
|
||||
transformIgnorePatterns: [
|
||||
// @chenglou/pretext is ESM-only; @signozhq/ui pulls it in via text-ellipsis.
|
||||
// Pattern 1: allow .pnpm virtual store through (handled by pattern 2), plus root-level ESM packages.
|
||||
'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|mdast-util-from-markdown|mdast-util-to-string|micromark|micromark-core-commonmark|micromark-extension-gfm|micromark-extension-gfm-autolink-literal|micromark-extension-gfm-footnote|micromark-extension-gfm-strikethrough|micromark-extension-gfm-table|micromark-extension-gfm-tagfilter|micromark-extension-gfm-task-list-item|micromark-factory-destination|micromark-factory-label|micromark-factory-space|micromark-factory-title|micromark-factory-whitespace|micromark-util-character|micromark-util-chunked|micromark-util-classify-character|micromark-util-combine-extensions|micromark-util-decode-numeric-character-reference|micromark-util-decode-string|micromark-util-encode|micromark-util-html-tag-name|micromark-util-normalize-identifier|micromark-util-resolve-all|micromark-util-sanitize-uri|micromark-util-subtokenize|micromark-util-symbol|micromark-util-types|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)/)',
|
||||
'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)/)',
|
||||
// Pattern 2: pnpm virtual store — ignore everything except ESM-only packages.
|
||||
// pnpm encodes scoped packages as @scope+name@version, so match on scope prefix.
|
||||
'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)[^/]*/node_modules)',
|
||||
'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)[^/]*/node_modules)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"@dnd-kit/modifiers": "7.0.0",
|
||||
"@dnd-kit/sortable": "8.0.0",
|
||||
"@dnd-kit/utilities": "3.2.2",
|
||||
"@grafana/data": "^11.6.15",
|
||||
"@grafana/data": "^11.6.14",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@sentry/react": "10.57.0",
|
||||
"@sentry/vite-plugin": "5.3.0",
|
||||
@@ -79,7 +79,7 @@
|
||||
"event-source-polyfill": "1.0.31",
|
||||
"eventemitter3": "5.0.1",
|
||||
"history": "4.10.1",
|
||||
"http-proxy-middleware": "4.1.1",
|
||||
"http-proxy-middleware": "4.0.0",
|
||||
"http-status-codes": "2.3.0",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
@@ -231,17 +231,16 @@
|
||||
"xml2js": "0.5.0",
|
||||
"phin": "^3.7.1",
|
||||
"body-parser": "1.20.3",
|
||||
"http-proxy-middleware": "4.1.1",
|
||||
"http-proxy-middleware": "4.0.0",
|
||||
"cross-spawn": "7.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"serialize-javascript": "6.0.2",
|
||||
"prismjs": "1.30.0",
|
||||
"got": "11.8.5",
|
||||
"form-data": "4.0.6",
|
||||
"form-data": "4.0.4",
|
||||
"brace-expansion": "^2.0.2",
|
||||
"on-headers": "^1.1.0",
|
||||
"js-cookie": "^3.0.7",
|
||||
"tmp": "0.2.7",
|
||||
"tmp": "0.2.4",
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
}
|
||||
}
|
||||
85
frontend/pnpm-lock.yaml
generated
85
frontend/pnpm-lock.yaml
generated
@@ -12,17 +12,16 @@ overrides:
|
||||
xml2js: 0.5.0
|
||||
phin: ^3.7.1
|
||||
body-parser: 1.20.3
|
||||
http-proxy-middleware: 4.1.1
|
||||
http-proxy-middleware: 4.0.0
|
||||
cross-spawn: 7.0.5
|
||||
cookie: ^0.7.1
|
||||
serialize-javascript: 6.0.2
|
||||
prismjs: 1.30.0
|
||||
got: 11.8.5
|
||||
form-data: 4.0.6
|
||||
form-data: 4.0.4
|
||||
brace-expansion: ^2.0.2
|
||||
on-headers: ^1.1.0
|
||||
js-cookie: ^3.0.7
|
||||
tmp: 0.2.7
|
||||
tmp: 0.2.4
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
|
||||
importers:
|
||||
@@ -57,8 +56,8 @@ importers:
|
||||
specifier: 3.2.2
|
||||
version: 3.2.2(react@18.2.0)
|
||||
'@grafana/data':
|
||||
specifier: ^11.6.15
|
||||
version: 11.6.15(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
specifier: ^11.6.14
|
||||
version: 11.6.14(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@monaco-editor/react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -165,8 +164,8 @@ importers:
|
||||
specifier: 4.10.1
|
||||
version: 4.10.1
|
||||
http-proxy-middleware:
|
||||
specifier: 4.1.1
|
||||
version: 4.1.1
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
http-status-codes:
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
@@ -1637,14 +1636,14 @@ packages:
|
||||
'@gerrit0/mini-shiki@3.23.0':
|
||||
resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==}
|
||||
|
||||
'@grafana/data@11.6.15':
|
||||
resolution: {integrity: sha512-q2Zbjr0N9iEGY/zKHm4Z4X5x64806E17W58y7mnvwc0MlbyGPPVulcp/rWA2Nd190mZeafZQPer9u+MaO+0HUQ==}
|
||||
'@grafana/data@11.6.14':
|
||||
resolution: {integrity: sha512-Nsjq1A9m6LbsKsKvOgvAk9Wq7RGjy0V4N9d5YsSnzMwCiw/ov2wblR2bcDpy95uF8KaDTIR2Gf40nJaOYksPMA==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
|
||||
'@grafana/schema@11.6.15':
|
||||
resolution: {integrity: sha512-MPIvGAp9uzkswnH6e+Fmzu+WBTqWMgbv93/8iu56gb+sjCB2LciZLz4KvrPFdw32bWCGSMAGqsML9mgmeJZtGQ==}
|
||||
'@grafana/schema@11.6.14':
|
||||
resolution: {integrity: sha512-YTqgYekb7kiu5NEoQxKF8czJ6QIARmMkCi9cNcynHqYpcDLOv5pg5Q0QtKgiiqHjlYoEeCV6iejdB4hXxzB+VA==}
|
||||
|
||||
'@humanfs/core@0.19.2':
|
||||
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
||||
@@ -5168,8 +5167,8 @@ packages:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.6:
|
||||
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
||||
form-data@4.0.4:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
format@0.2.2:
|
||||
@@ -5382,10 +5381,6 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.4:
|
||||
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hast-util-from-parse5@8.0.1:
|
||||
resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==}
|
||||
|
||||
@@ -5461,8 +5456,8 @@ packages:
|
||||
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
http-proxy-middleware@4.1.1:
|
||||
resolution: {integrity: sha512-KX5ZofGXLFXqFAkQoOWZ+rTtaLTut7m0gyL+QzJrdejtIZ+F4bPPDoe7reISg2+v0CAz5OfVwEJEhty7X+e57g==}
|
||||
http-proxy-middleware@4.0.0:
|
||||
resolution: {integrity: sha512-wuHwaUtmC0XzJNHqRp41zXtt5ojpHbusXGhq6781VvnjWUYPu7opmOF3eomGNujT07kEOnHWZyV9UZzKimVCKA==}
|
||||
engines: {node: ^22.15.0 || ^24.0.0 || >=26.0.0}
|
||||
|
||||
http-status-codes@2.3.0:
|
||||
@@ -5472,8 +5467,8 @@ packages:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
httpxy@0.5.3:
|
||||
resolution: {integrity: sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw==}
|
||||
httpxy@0.5.1:
|
||||
resolution: {integrity: sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A==}
|
||||
|
||||
human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
@@ -6046,8 +6041,8 @@ packages:
|
||||
js-base64@3.7.5:
|
||||
resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==}
|
||||
|
||||
js-cookie@3.0.8:
|
||||
resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==}
|
||||
js-cookie@2.2.1:
|
||||
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
|
||||
|
||||
js-levenshtein@1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
@@ -8399,8 +8394,8 @@ packages:
|
||||
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
|
||||
tmp@0.2.7:
|
||||
resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==}
|
||||
tmp@0.2.4:
|
||||
resolution: {integrity: sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
tmpl@1.0.5:
|
||||
@@ -10323,10 +10318,10 @@ snapshots:
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@grafana/data@11.6.15(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
'@grafana/data@11.6.14(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@braintree/sanitize-url': 7.0.1
|
||||
'@grafana/schema': 11.6.15
|
||||
'@grafana/schema': 11.6.14
|
||||
'@types/d3-interpolate': 3.0.1
|
||||
'@types/string-hash': 1.1.3
|
||||
d3-interpolate: 3.0.1
|
||||
@@ -10352,7 +10347,7 @@ snapshots:
|
||||
uplot: 1.6.31
|
||||
xss: 1.0.14
|
||||
|
||||
'@grafana/schema@11.6.15':
|
||||
'@grafana/schema@11.6.14':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
@@ -12891,7 +12886,7 @@ snapshots:
|
||||
axios@1.16.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0
|
||||
form-data: 4.0.6
|
||||
form-data: 4.0.4
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
@@ -13838,7 +13833,7 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.4
|
||||
hasown: 2.0.2
|
||||
|
||||
es-toolkit@1.46.1: {}
|
||||
|
||||
@@ -14036,7 +14031,7 @@ snapshots:
|
||||
dependencies:
|
||||
chardet: 0.7.0
|
||||
iconv-lite: 0.4.24
|
||||
tmp: 0.2.7
|
||||
tmp: 0.2.4
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
@@ -14169,12 +14164,12 @@ snapshots:
|
||||
cross-spawn: 7.0.5
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.6:
|
||||
form-data@4.0.4:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.4
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
format@0.2.2: {}
|
||||
@@ -14253,7 +14248,7 @@ snapshots:
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.4
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
@@ -14391,10 +14386,6 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hasown@2.0.4:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hast-util-from-parse5@8.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@@ -14515,10 +14506,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-proxy-middleware@4.1.1:
|
||||
http-proxy-middleware@4.0.0:
|
||||
dependencies:
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
httpxy: 0.5.3
|
||||
httpxy: 0.5.1
|
||||
is-glob: 4.0.3
|
||||
is-plain-obj: 4.1.0
|
||||
micromatch: 4.0.8
|
||||
@@ -14534,7 +14525,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
httpxy@0.5.3: {}
|
||||
httpxy@0.5.1: {}
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
@@ -15348,7 +15339,7 @@ snapshots:
|
||||
|
||||
js-base64@3.7.5: {}
|
||||
|
||||
js-cookie@3.0.8: {}
|
||||
js-cookie@2.2.1: {}
|
||||
|
||||
js-levenshtein@1.1.6: {}
|
||||
|
||||
@@ -15376,7 +15367,7 @@ snapshots:
|
||||
decimal.js: 10.6.0
|
||||
domexception: 4.0.0
|
||||
escodegen: 2.1.0
|
||||
form-data: 4.0.6
|
||||
form-data: 4.0.4
|
||||
html-encoding-sniffer: 3.0.0
|
||||
http-proxy-agent: 5.0.0
|
||||
https-proxy-agent: 5.0.1
|
||||
@@ -17345,7 +17336,7 @@ snapshots:
|
||||
copy-to-clipboard: 3.3.3
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-shallow-equal: 1.0.0
|
||||
js-cookie: 3.0.8
|
||||
js-cookie: 2.2.1
|
||||
nano-css: 5.6.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
@@ -17364,7 +17355,7 @@ snapshots:
|
||||
copy-to-clipboard: 3.3.3
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-shallow-equal: 1.0.0
|
||||
js-cookie: 3.0.8
|
||||
js-cookie: 2.2.1
|
||||
nano-css: 5.6.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
@@ -18112,7 +18103,7 @@ snapshots:
|
||||
|
||||
tinypool@2.1.0: {}
|
||||
|
||||
tmp@0.2.7: {}
|
||||
tmp@0.2.4: {}
|
||||
|
||||
tmpl@1.0.5: {}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
),
|
||||
[pathname],
|
||||
);
|
||||
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
||||
const currentRoute = mapRoutes.get('current');
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
@@ -82,36 +83,12 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
}, [usersData?.data]);
|
||||
|
||||
// Handle old routes - redirect to new routes
|
||||
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
||||
if (isOldRoute) {
|
||||
const redirectUrl = oldNewRoutesMapping[pathname];
|
||||
// TODO(H4ad): Remove this after https://github.com/SigNoz/engineering-pod/issues/5322
|
||||
// A mapped target may itself carry a query string (e.g. `/alerts?tab=Channels`).
|
||||
// react-router does not re-parse a `?` embedded in the `pathname` field, so split
|
||||
// it out and merge with the incoming search params.
|
||||
const [redirectPath, redirectSearch = ''] = redirectUrl.split('?');
|
||||
const mergedParams = new URLSearchParams(location.search);
|
||||
new URLSearchParams(redirectSearch).forEach((value, name) => {
|
||||
mergedParams.set(name, value);
|
||||
});
|
||||
const search = mergedParams.toString();
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: redirectPath,
|
||||
search: search ? `?${search}` : '',
|
||||
hash: location.hash,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (pathname.startsWith('/settings/channels/edit/')) {
|
||||
const channelId = pathname.replace('/settings/channels/edit/', '');
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: `/alerts/channels/edit/${channelId}`,
|
||||
pathname: redirectUrl,
|
||||
search: location.search,
|
||||
hash: location.hash,
|
||||
}}
|
||||
|
||||
@@ -73,13 +73,7 @@ const queryClient = new QueryClient({
|
||||
// Component to capture current location for assertions
|
||||
function LocationDisplay(): ReactElement {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<>
|
||||
<div data-testid="location-display">{location.pathname}</div>
|
||||
<div data-testid="location-search">{location.search}</div>
|
||||
<div data-testid="location-hash">{location.hash}</div>
|
||||
</>
|
||||
);
|
||||
return <div data-testid="location-display">{location.pathname}</div>;
|
||||
}
|
||||
|
||||
// Helper to create mock user
|
||||
@@ -1481,10 +1475,12 @@ describe('PrivateRoute', () => {
|
||||
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
|
||||
});
|
||||
|
||||
it('should redirect VIEWER from /alerts/channels/new (ADMIN only)', async () => {
|
||||
// After moving channels under /alerts, CHANNELS_NEW ('/alerts/channels/new')
|
||||
// is an exact, ADMIN-only route with no overlapping non-exact ALL_CHANNELS
|
||||
// route to match last, so a VIEWER is now correctly redirected.
|
||||
it('should not redirect VIEWER from /settings/channels/new due to route matching order (ALL_CHANNELS matches last)', () => {
|
||||
// Note: This tests the ACTUAL behavior of Private.tsx route matching
|
||||
// CHANNELS_NEW has path '/settings/channels/new' with permission ['ADMIN']
|
||||
// ALL_CHANNELS has path '/settings/channels' with permission ['ADMIN', 'EDITOR', 'VIEWER']
|
||||
// Due to non-exact matching and array order, ALL_CHANNELS matches LAST for '/settings/channels/new'
|
||||
// This is a known limitation - actual permission enforcement happens in the page component
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.CHANNELS_NEW,
|
||||
appContext: {
|
||||
@@ -1493,7 +1489,8 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
|
||||
assertRendersChildren();
|
||||
assertStaysOnRoute(ROUTES.CHANNELS_NEW);
|
||||
});
|
||||
|
||||
it('should allow EDITOR to access /get-started route', () => {
|
||||
@@ -1551,60 +1548,4 @@ describe('PrivateRoute', () => {
|
||||
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Old channel route redirects', () => {
|
||||
it.each([
|
||||
['/settings/channels', '/alerts', 'tab=Channels'],
|
||||
['/settings/channels/new', '/alerts/channels/new', ''],
|
||||
])(
|
||||
'should redirect %s to %s',
|
||||
async (oldRoute, expectedPath, expectedSearch) => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: oldRoute,
|
||||
appContext: { isLoggedIn: true },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
expectedPath,
|
||||
);
|
||||
});
|
||||
|
||||
if (expectedSearch) {
|
||||
const search = screen.getByTestId('location-search').textContent ?? '';
|
||||
const params = new URLSearchParams(search);
|
||||
new URLSearchParams(expectedSearch).forEach((value, name) => {
|
||||
expect(params.get(name)).toBe(value);
|
||||
});
|
||||
} else {
|
||||
expect(screen.getByTestId('location-search')).toHaveTextContent('');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it('should redirect dynamic channel edit route preserving the channel id', async () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: '/settings/channels/edit/abc123',
|
||||
appContext: { isLoggedIn: true },
|
||||
});
|
||||
|
||||
await assertRedirectsTo('/alerts/channels/edit/abc123');
|
||||
});
|
||||
|
||||
it('should merge incoming query params with the embedded query of the target', async () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: '/settings/channels?foo=bar',
|
||||
appContext: { isLoggedIn: true },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent('/alerts');
|
||||
});
|
||||
|
||||
const search = screen.getByTestId('location-search').textContent ?? '';
|
||||
const params = new URLSearchParams(search);
|
||||
expect(params.get('tab')).toBe('Channels');
|
||||
expect(params.get('foo')).toBe('bar');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,12 +142,12 @@ export const AlertOverview = Loadable(
|
||||
() => import(/* webpackChunkName: "Alert Overview" */ 'pages/AlertList'),
|
||||
);
|
||||
|
||||
export const ChannelsNew = Loadable(
|
||||
() => import(/* webpackChunkName: "Create Channels" */ 'pages/AlertList'),
|
||||
export const CreateAlertChannelAlerts = Loadable(
|
||||
() => import(/* webpackChunkName: "Create Channels" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const ChannelsEdit = Loadable(
|
||||
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/AlertList'),
|
||||
export const AllAlertChannels = Loadable(
|
||||
() => import(/* webpackChunkName: "All Channels" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const AllErrors = Loadable(
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
AIAssistantPage,
|
||||
AlertHistory,
|
||||
AlertOverview,
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
ApiMonitoring,
|
||||
ChannelsEdit,
|
||||
ChannelsNew,
|
||||
CreateAlertChannelAlerts,
|
||||
CreateNewAlerts,
|
||||
DashboardPage,
|
||||
DashboardsListPage,
|
||||
@@ -269,16 +269,16 @@ const routes: AppRoutes[] = [
|
||||
{
|
||||
path: ROUTES.CHANNELS_NEW,
|
||||
exact: true,
|
||||
component: ChannelsNew,
|
||||
component: CreateAlertChannelAlerts,
|
||||
isPrivate: true,
|
||||
key: 'CHANNELS_NEW',
|
||||
},
|
||||
{
|
||||
path: ROUTES.CHANNELS_EDIT,
|
||||
path: ROUTES.ALL_CHANNELS,
|
||||
exact: true,
|
||||
component: ChannelsEdit,
|
||||
component: AllAlertChannels,
|
||||
isPrivate: true,
|
||||
key: 'CHANNELS_EDIT',
|
||||
key: 'ALL_CHANNELS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALL_ERROR,
|
||||
@@ -534,9 +534,6 @@ export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/messaging-queues': '/messaging-queues/overview',
|
||||
'/alerts/edit': '/alerts/overview',
|
||||
'/alerts/type-selection': '/alerts/new',
|
||||
// TODO(H4ad): Update this after https://github.com/SigNoz/engineering-pod/issues/5322
|
||||
'/settings/channels': '/alerts?tab=Channels',
|
||||
'/settings/channels/new': '/alerts/channels/new',
|
||||
};
|
||||
export const oldRoutes = Object.keys(oldNewRoutesMapping);
|
||||
|
||||
|
||||
@@ -4,14 +4,22 @@
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation } from 'react-query';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
GetOnboarding200,
|
||||
GetOnboardingParams,
|
||||
InframonitoringtypesPostableClustersDTO,
|
||||
InframonitoringtypesPostableDaemonSetsDTO,
|
||||
InframonitoringtypesPostableDeploymentsDTO,
|
||||
@@ -619,6 +627,104 @@ export const useListNodes = <
|
||||
> => {
|
||||
return useMutation(getListNodesMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
* @summary Get Onboarding Status for Infra Monitoring
|
||||
*/
|
||||
export const getOnboarding = (
|
||||
params: GetOnboardingParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetOnboarding200>({
|
||||
url: `/api/v2/infra_monitoring/onboarding`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetOnboardingQueryKey = (params?: GetOnboardingParams) => {
|
||||
return [
|
||||
`/api/v2/infra_monitoring/onboarding`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetOnboardingQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getOnboarding>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: GetOnboardingParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getOnboarding>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetOnboardingQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getOnboarding>>> = ({
|
||||
signal,
|
||||
}) => getOnboarding(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getOnboarding>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetOnboardingQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getOnboarding>>
|
||||
>;
|
||||
export type GetOnboardingQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get Onboarding Status for Infra Monitoring
|
||||
*/
|
||||
|
||||
export function useGetOnboarding<
|
||||
TData = Awaited<ReturnType<typeof getOnboarding>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: GetOnboardingParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getOnboarding>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetOnboardingQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get Onboarding Status for Infra Monitoring
|
||||
*/
|
||||
export const invalidateGetOnboarding = async (
|
||||
queryClient: QueryClient,
|
||||
params: GetOnboardingParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetOnboardingQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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/unknown/no_data), 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 six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
|
||||
* @summary List Pods for Infra Monitoring
|
||||
|
||||
@@ -5346,6 +5346,26 @@ export interface GlobaltypesConfigDTO {
|
||||
mcp_url: string | null;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesOnboardingComponentTypeDTO {
|
||||
receiver = 'receiver',
|
||||
processor = 'processor',
|
||||
}
|
||||
export interface InframonitoringtypesAssociatedComponentDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
type: InframonitoringtypesOnboardingComponentTypeDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesAttributesComponentEntryDTO {
|
||||
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
attributes: string[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -5802,6 +5822,46 @@ export interface InframonitoringtypesJobsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesMetricsComponentEntryDTO {
|
||||
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
metrics: string[] | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesMissingAttributesComponentEntryDTO {
|
||||
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
attributes: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
documentationLink: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesMissingMetricsComponentEntryDTO {
|
||||
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
documentationLink: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
metrics: string[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -5919,6 +5979,61 @@ export interface InframonitoringtypesNodesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesOnboardingTypeDTO {
|
||||
hosts = 'hosts',
|
||||
processes = 'processes',
|
||||
pods = 'pods',
|
||||
nodes = 'nodes',
|
||||
deployments = 'deployments',
|
||||
daemonsets = 'daemonsets',
|
||||
statefulsets = 'statefulsets',
|
||||
jobs = 'jobs',
|
||||
namespaces = 'namespaces',
|
||||
clusters = 'clusters',
|
||||
volumes = 'volumes',
|
||||
}
|
||||
export interface InframonitoringtypesOnboardingDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
missingDefaultEnabledMetrics:
|
||||
| InframonitoringtypesMissingMetricsComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
missingOptionalMetrics:
|
||||
| InframonitoringtypesMissingMetricsComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
missingRequiredAttributes:
|
||||
| InframonitoringtypesMissingAttributesComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
presentDefaultEnabledMetrics:
|
||||
| InframonitoringtypesMetricsComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
presentOptionalMetrics: InframonitoringtypesMetricsComponentEntryDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
presentRequiredAttributes:
|
||||
| InframonitoringtypesAttributesComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
ready: boolean;
|
||||
type: InframonitoringtypesOnboardingTypeDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesPodPhaseDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
@@ -10177,6 +10292,21 @@ export type ListNodes200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetOnboardingParams = {
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
type: InframonitoringtypesOnboardingTypeDTO;
|
||||
};
|
||||
|
||||
export type GetOnboarding200 = {
|
||||
data: InframonitoringtypesOnboardingDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListPods200 = {
|
||||
data: InframonitoringtypesPodsDTO;
|
||||
/**
|
||||
|
||||
@@ -29,10 +29,9 @@ const ROUTES = {
|
||||
ALERTS_NEW: '/alerts/new',
|
||||
ALERT_HISTORY: '/alerts/history',
|
||||
ALERT_OVERVIEW: '/alerts/overview',
|
||||
// TODO(H4ad): Add test to forbidden ? in this map after https://github.com/SigNoz/engineering-pod/issues/5322
|
||||
ALL_CHANNELS: '/alerts?tab=Channels',
|
||||
CHANNELS_NEW: '/alerts/channels/new',
|
||||
CHANNELS_EDIT: '/alerts/channels/edit/:channelId',
|
||||
ALL_CHANNELS: '/settings/channels',
|
||||
CHANNELS_NEW: '/settings/channels/new',
|
||||
CHANNELS_EDIT: '/settings/channels/edit/:channelId',
|
||||
ALL_ERROR: '/exceptions',
|
||||
ERROR_DETAIL: '/error-detail',
|
||||
VERSION: '/status',
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('resourceRoute', () => {
|
||||
|
||||
it('routes channels to the edit page', () => {
|
||||
expect(resourceRoute(ResourceType.channel, 'channel-uuid-1')).toBe(
|
||||
'/alerts/channels/edit/channel-uuid-1',
|
||||
'/settings/channels/edit/channel-uuid-1',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.alert-channels-container {
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-8);
|
||||
width: 90%;
|
||||
margin: 12px auto;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.create-alert-channels-container {
|
||||
width: 100%;
|
||||
width: 90%;
|
||||
margin: 12px auto;
|
||||
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -80,7 +80,7 @@ import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
|
||||
|
||||
import { useCmdK } from '../../providers/cmdKProvider';
|
||||
import { routeConfig } from './config';
|
||||
import { buildNavUrl, getQueryString } from './helper';
|
||||
import { getQueryString } from './helper';
|
||||
import {
|
||||
defaultMoreMenuItems,
|
||||
getUserSettingsDropdownMenuItems,
|
||||
@@ -486,13 +486,12 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const availableParams = routeConfig[key];
|
||||
|
||||
const queryString = getQueryString(availableParams || [], params);
|
||||
const url = buildNavUrl(key, queryString);
|
||||
|
||||
if (pathname !== key) {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(url);
|
||||
openInNewTab(`${key}?${queryString.join('&')}`);
|
||||
} else {
|
||||
history.push(url, {
|
||||
history.push(`${key}?${queryString.join('&')}`, {
|
||||
from: pathname,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,14 +8,3 @@ export const getQueryString = (
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated This should be removed after https://github.com/SigNoz/engineering-pod/issues/5322 is done
|
||||
*/
|
||||
export const buildNavUrl = (key: string, queryString: string[]): string => {
|
||||
if (key.includes('?')) {
|
||||
const extra = queryString.filter(Boolean).join('&');
|
||||
return extra ? `${key}&${extra}` : key;
|
||||
}
|
||||
return `${key}?${queryString.join('&')}`;
|
||||
};
|
||||
|
||||
@@ -337,7 +337,6 @@ export const settingsNavSections: SettingsNavSection[] = [
|
||||
isEnabled: true,
|
||||
itemKey: 'account',
|
||||
},
|
||||
// TODO(@SigNoz/pulse-frontend): https://github.com/SigNoz/engineering-pod/issues/5323
|
||||
{
|
||||
key: ROUTES.ALL_CHANNELS,
|
||||
label: 'Notification Channels',
|
||||
|
||||
@@ -4,17 +4,14 @@ import { Tabs, TabsProps } from 'antd';
|
||||
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import ROUTES from 'constants/routes';
|
||||
import AllAlertChannels from 'container/AllAlertChannels';
|
||||
import AllAlertRules from 'container/ListAlertRules';
|
||||
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
|
||||
import RoutingPolicies from 'container/RoutingPolicies';
|
||||
import TriggeredAlerts from 'container/TriggeredAlerts';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Cable, GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
|
||||
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
|
||||
import AlertDetails from 'pages/AlertDetails';
|
||||
import ChannelsEdit from 'pages/ChannelsEdit';
|
||||
import ChannelsNew from 'pages/ChannelsNew';
|
||||
|
||||
import { AlertListSubTabs, AlertListTabs } from './types';
|
||||
|
||||
@@ -29,9 +26,6 @@ function AllAlertList(): JSX.Element {
|
||||
const subTab = urlQuery.get('subTab');
|
||||
const isAlertHistory = location.pathname === ROUTES.ALERT_HISTORY;
|
||||
const isAlertOverview = location.pathname === ROUTES.ALERT_OVERVIEW;
|
||||
const isChannelsNew = location.pathname === ROUTES.CHANNELS_NEW;
|
||||
const isChannelsEdit = location.pathname.startsWith('/alerts/channels/edit/');
|
||||
const isChannelDetails = isChannelsNew || isChannelsEdit;
|
||||
|
||||
const handleConfigurationTabChange = useCallback(
|
||||
(subTab: string): void => {
|
||||
@@ -92,22 +86,6 @@ function AllAlertList(): JSX.Element {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<Cable size={14} />
|
||||
Notification Channels
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.CHANNELS,
|
||||
children: (
|
||||
<div className="alert-rules-container">
|
||||
{isChannelsNew && <ChannelsNew />}
|
||||
{isChannelsEdit && <ChannelsEdit />}
|
||||
{!isChannelDetails && <AllAlertChannels />}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
@@ -120,21 +98,11 @@ function AllAlertList(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const getActiveKey = (): string => {
|
||||
if (isAlertHistory || isAlertOverview) {
|
||||
return AlertListTabs.ALERT_RULES;
|
||||
}
|
||||
if (isChannelDetails) {
|
||||
return AlertListTabs.CHANNELS;
|
||||
}
|
||||
return tab || AlertListTabs.ALERT_RULES;
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
destroyInactiveTabPane
|
||||
items={items}
|
||||
activeKey={getActiveKey()}
|
||||
activeKey={tab || AlertListTabs.ALERT_RULES}
|
||||
onChange={(tab): void => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
@@ -152,9 +120,7 @@ function AllAlertList(): JSX.Element {
|
||||
safeNavigate(`/alerts?${queryParams.toString()}`);
|
||||
}}
|
||||
className={`alerts-container ${
|
||||
isAlertHistory || isAlertOverview || isChannelDetails
|
||||
? 'alert-details-tabs'
|
||||
: ''
|
||||
isAlertHistory || isAlertOverview ? 'alert-details-tabs' : ''
|
||||
}`}
|
||||
tabBarExtraContent={
|
||||
<HeaderRightSection
|
||||
|
||||
@@ -7,5 +7,4 @@ export enum AlertListTabs {
|
||||
TRIGGERED_ALERTS = 'TriggeredAlerts',
|
||||
ALERT_RULES = 'AlertRules',
|
||||
CONFIGURATION = 'Configuration',
|
||||
CHANNELS = 'Channels',
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import get from 'api/channels/get';
|
||||
import AlertBreadcrumb from 'components/AlertBreadcrumb';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ChannelType,
|
||||
MsTeamsChannel,
|
||||
@@ -24,9 +22,9 @@ import './ChannelsEdit.styles.scss';
|
||||
function ChannelsEdit(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Extract channelId from URL pathname
|
||||
// Extract channelId from URL pathname since useParams doesn't work in nested routing
|
||||
const { pathname } = window.location;
|
||||
const channelIdMatch = pathname.match(/\/alerts\/channels\/edit\/([^/]+)/);
|
||||
const channelIdMatch = pathname.match(/\/settings\/channels\/edit\/([^/]+)/);
|
||||
const channelId = channelIdMatch ? channelIdMatch[1] : undefined;
|
||||
|
||||
const { isFetching, isError, data, error } = useQuery<
|
||||
@@ -137,25 +135,17 @@ function ChannelsEdit(): JSX.Element {
|
||||
const target = prepChannelConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertBreadcrumb
|
||||
items={[
|
||||
{ title: 'Channels', route: ROUTES.ALL_CHANNELS },
|
||||
{ title: value.name || 'Edit Channel', isLast: true },
|
||||
]}
|
||||
<div className="edit-alert-channels-container">
|
||||
<EditAlertChannels
|
||||
{...{
|
||||
initialValue: {
|
||||
...target.channel,
|
||||
type: target.type,
|
||||
name: value.name,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div className="edit-alert-channels-container">
|
||||
<EditAlertChannels
|
||||
{...{
|
||||
initialValue: {
|
||||
...target.channel,
|
||||
type: target.type,
|
||||
name: value.name,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import AlertBreadcrumb from 'components/AlertBreadcrumb';
|
||||
import ROUTES from 'constants/routes';
|
||||
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||
import { ChannelType } from 'container/CreateAlertChannels/config';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
function ChannelsNew(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<AlertBreadcrumb
|
||||
items={[
|
||||
{ title: 'Channels', route: ROUTES.ALL_CHANNELS },
|
||||
{ title: 'New Channel', isLast: true },
|
||||
]}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<CreateAlertChannels preType={ChannelType.Slack} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelsNew;
|
||||
@@ -1,4 +0,0 @@
|
||||
.content {
|
||||
padding: var(--spacing-8);
|
||||
padding-top: 0px;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import RouteTab from 'components/RouteTab';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { routeConfig } from 'container/SideNav/config';
|
||||
import { buildNavUrl, getQueryString } from 'container/SideNav/helper';
|
||||
import { getQueryString } from 'container/SideNav/helper';
|
||||
import { settingsNavSections } from 'container/SideNav/menuItems';
|
||||
import NavItem from 'container/SideNav/NavItem/NavItem';
|
||||
import { SidebarItem } from 'container/SideNav/sideNav.types';
|
||||
@@ -240,13 +240,12 @@ function SettingsPage(): JSX.Element {
|
||||
const availableParams = routeConfig[key];
|
||||
|
||||
const queryString = getQueryString(availableParams || [], params);
|
||||
const url = buildNavUrl(key, queryString);
|
||||
|
||||
if (pathname !== key) {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(url);
|
||||
openInNewTab(`${key}?${queryString.join('&')}`);
|
||||
} else {
|
||||
history.push(url, {
|
||||
history.push(`${key}?${queryString.join('&')}`, {
|
||||
from: pathname,
|
||||
});
|
||||
}
|
||||
@@ -260,6 +259,17 @@ function SettingsPage(): JSX.Element {
|
||||
};
|
||||
|
||||
const isActiveNavItem = (key: string): boolean => {
|
||||
if (pathname.startsWith(ROUTES.ALL_CHANNELS) && key === ROUTES.ALL_CHANNELS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
pathname.startsWith(ROUTES.CHANNELS_EDIT) &&
|
||||
key === ROUTES.ALL_CHANNELS
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
pathname.startsWith(ROUTES.ROLES_SETTINGS) &&
|
||||
key === ROUTES.ROLES_SETTINGS
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { RouteTabProps } from 'components/RouteTab/types';
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import BillingContainer from 'container/BillingContainer/BillingContainer';
|
||||
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||
import { ChannelType } from 'container/CreateAlertChannels/config';
|
||||
import GeneralSettings from 'container/GeneralSettings';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import IngestionSettings from 'container/IngestionSettings/IngestionSettings';
|
||||
@@ -13,16 +16,20 @@ import RoleDetailsPage from 'container/RolesSettings/RoleDetails';
|
||||
import { TFunction } from 'i18next';
|
||||
import {
|
||||
Backpack,
|
||||
BellDot,
|
||||
Bot,
|
||||
Building,
|
||||
Cpu,
|
||||
CreditCard,
|
||||
Keyboard,
|
||||
Pencil,
|
||||
Plus,
|
||||
Shield,
|
||||
Sparkles,
|
||||
User,
|
||||
Users,
|
||||
} from '@signozhq/icons';
|
||||
import ChannelsEdit from 'pages/ChannelsEdit';
|
||||
import MembersSettings from 'pages/MembersSettings';
|
||||
import ServiceAccountsSettings from 'pages/ServiceAccountsSettings';
|
||||
import Shortcuts from 'pages/Shortcuts';
|
||||
@@ -40,6 +47,19 @@ export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
},
|
||||
];
|
||||
|
||||
export const alertChannels = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: AlertChannels,
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<BellDot size={16} /> {t('routes:alert_channels').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.ALL_CHANNELS,
|
||||
key: ROUTES.ALL_CHANNELS,
|
||||
},
|
||||
];
|
||||
|
||||
export const ingestionSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: IngestionSettings,
|
||||
@@ -199,3 +219,31 @@ export const mcpServerSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
key: ROUTES.MCP_SERVER,
|
||||
},
|
||||
];
|
||||
|
||||
export const createAlertChannels = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: (): JSX.Element => (
|
||||
<CreateAlertChannels preType={ChannelType.Slack} />
|
||||
),
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<Plus size={16} /> {t('routes:create_alert_channels').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.CHANNELS_NEW,
|
||||
key: ROUTES.CHANNELS_NEW,
|
||||
},
|
||||
];
|
||||
|
||||
export const editAlertChannels = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: ChannelsEdit,
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<Pencil size={16} /> {t('routes:edit_alert_channels').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.CHANNELS_EDIT,
|
||||
key: ROUTES.CHANNELS_EDIT,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,7 +3,10 @@ import { TFunction } from 'i18next';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
|
||||
import {
|
||||
alertChannels,
|
||||
billingSettings,
|
||||
createAlertChannels,
|
||||
editAlertChannels,
|
||||
generalSettings,
|
||||
ingestionSettings,
|
||||
keyboardShortcuts,
|
||||
@@ -57,6 +60,8 @@ export const getRoutes = (
|
||||
settings.push(...ingestionSettings(t));
|
||||
}
|
||||
|
||||
settings.push(...alertChannels(t));
|
||||
|
||||
// Visible to all authenticated users
|
||||
settings.push(
|
||||
...serviceAccountsSettings(t),
|
||||
@@ -75,6 +80,8 @@ export const getRoutes = (
|
||||
|
||||
settings.push(
|
||||
...mySettings(t),
|
||||
...createAlertChannels(t),
|
||||
...editAlertChannels(t),
|
||||
...keyboardShortcuts(t),
|
||||
...mcpServerSettings(t),
|
||||
);
|
||||
|
||||
@@ -200,5 +200,23 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/infra_monitoring/onboarding", handler.New(
|
||||
provider.authzMiddleware.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
|
||||
}
|
||||
|
||||
@@ -22,6 +22,30 @@ func NewHandler(m inframonitoring.Module) inframonitoring.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (h *handler) ListHosts(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
|
||||
@@ -473,6 +473,96 @@ func (m *module) getMetricsExistenceAndEarliestTime(ctx context.Context, metricN
|
||||
return missingMetrics, globalMinFirstReported, nil
|
||||
}
|
||||
|
||||
// getMetricsExistence returns, for each requested metric name, whether it has ever
|
||||
// been reported (present in signoz_metrics.distributed_metadata). No time window.
|
||||
func (m *module) getMetricsExistence(ctx context.Context, metricNames []string) (map[string]bool, error) {
|
||||
present := make(map[string]bool, len(metricNames))
|
||||
for _, n := range metricNames {
|
||||
present[n] = false
|
||||
}
|
||||
if len(metricNames) == 0 {
|
||||
return present, nil
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("metric_name", "count(*) AS cnt")
|
||||
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, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var cnt uint64
|
||||
if err := rows.Scan(&name, &cnt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cnt > 0 {
|
||||
present[name] = true
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return present, nil
|
||||
}
|
||||
|
||||
// getAttributesExistence returns, for each requested attrName, whether it has ever
|
||||
// been reported as a label on any of the given metricNames. Presence is checked
|
||||
// against distributed_metadata without a time-range filter.
|
||||
func (m *module) getAttributesExistence(ctx context.Context, metricNames, attrNames []string) (map[string]bool, error) {
|
||||
present := make(map[string]bool, len(attrNames))
|
||||
for _, a := range attrNames {
|
||||
present[a] = false
|
||||
}
|
||||
if len(attrNames) == 0 {
|
||||
return present, nil
|
||||
}
|
||||
if len(metricNames) == 0 {
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "getAttributesExistence: metricNames must not be empty")
|
||||
}
|
||||
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()
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package implinframonitoring
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -29,3 +31,50 @@ type nodeConditionCounts struct {
|
||||
Ready int
|
||||
NotReady int
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (s onboardingSpec) getAllMetrics() []string {
|
||||
var out []string
|
||||
for _, b := range s.Buckets {
|
||||
out = append(out, b.DefaultMetrics...)
|
||||
out = append(out, b.OptionalMetrics...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s onboardingSpec) getAllAttrs() []string {
|
||||
var out []string
|
||||
for _, b := range s.Buckets {
|
||||
out = append(out, b.RequiredAttrs...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -49,6 +49,84 @@ func NewModule(
|
||||
}
|
||||
}
|
||||
|
||||
// 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, err := getSpecForType(req.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allMetrics := spec.getAllMetrics()
|
||||
allAttrs := spec.getAllAttrs()
|
||||
|
||||
presentMetrics, err := m.getMetricsExistence(ctx, allMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
missingMetricsMap := make(map[string]bool, len(allMetrics))
|
||||
for _, name := range allMetrics {
|
||||
if !presentMetrics[name] {
|
||||
missingMetricsMap[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
presentAttrs, err := m.getAttributesExistence(ctx, allMetrics, allAttrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
missingAttrsMap := make(map[string]bool, len(allAttrs))
|
||||
for _, name := range allAttrs {
|
||||
if !presentAttrs[name] {
|
||||
missingAttrsMap[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
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, missingMetricsMap, missingAttrsMap)
|
||||
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
|
||||
}
|
||||
|
||||
func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableHosts) (*inframonitoringtypes.Hosts, error) {
|
||||
ctx = m.withInfraMonitoringContext(ctx, "ListHosts")
|
||||
|
||||
|
||||
114
pkg/modules/inframonitoring/implinframonitoring/onboarding.go
Normal file
114
pkg/modules/inframonitoring/implinframonitoring/onboarding.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
)
|
||||
|
||||
// splitBucket partitions one component bucket's metric and attribute lists
|
||||
// against the module-wide missing sets into up to six response entries.
|
||||
// Empty partitions are left nil so callers can skip them.
|
||||
func splitBucket(b onboardingComponentBucket, missingMetrics, missingAttrs map[string]bool) bucketSplit {
|
||||
var s bucketSplit
|
||||
presentDef, missDef := partitionList(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 := partitionList(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 := partitionList(b.RequiredAttrs, missingAttrs)
|
||||
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
|
||||
}
|
||||
|
||||
// getSpecForType returns the onboardingSpec for a given OnboardingType, or an error if the type is invalid.
|
||||
func getSpecForType(t inframonitoringtypes.OnboardingType) (*onboardingSpec, error) {
|
||||
spec, ok := onboardingSpecs[t]
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "no onboarding spec for type: %s", t)
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// partitionList splits items into those NOT in `missing` and those in `missing`.
|
||||
// Preserves input order.
|
||||
func partitionList(items []string, missing map[string]bool) (present, miss []string) {
|
||||
for _, x := range items {
|
||||
if missing[x] {
|
||||
miss = append(miss, x)
|
||||
} else {
|
||||
present = append(present, x)
|
||||
}
|
||||
}
|
||||
return present, miss
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Sprintf(
|
||||
"Missing required attributes %s from %s. Learn how to configure here.",
|
||||
strings.Join(attrs, ", "), componentName,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
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/user-guides/hostmetrics/#configure-the-hostmetrics-receiver"
|
||||
docLinkKubeletStatsReceiver = "https://signoz.io/docs/infrastructure-monitoring/user-guides/k8s-metrics/#setup-kubelet-stats-receiver"
|
||||
docLinkK8sClusterReceiver = "https://signoz.io/docs/infrastructure-monitoring/user-guides/k8s-metrics/#setup-k8s-cluster-receiver"
|
||||
docLinkResourceDetectionProcessor = "https://signoz.io/docs/infrastructure-monitoring/user-guides/hostmetrics/#configure-the-resourcedetection-processor"
|
||||
docLinkK8sAttributesProcessor = "https://signoz.io/docs/infrastructure-monitoring/user-guides/k8s-metrics/#3-setup-k8sattributesprocessor-to-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,
|
||||
}
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
// 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.allocatable_cpu and k8s.node.allocatable_memory are
|
||||
// controlled by allocatable_types_to_report config option (Check // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L805-L806)
|
||||
"k8s.node.condition_ready", // # k8s.node.condition_* metrics (k8s.node.condition_ready, k8s.node.condition_memory_pressure, etc) are controlled# by node_conditions_to_report config option.
|
||||
// By default, only k8s.node.condition_ready is enabled. (Check https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L802)
|
||||
"k8s.pod.phase", // pod counts per node by phase
|
||||
},
|
||||
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.pod.phase",
|
||||
"k8s.deployment.desired",
|
||||
"k8s.deployment.available",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.deployment.name", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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.pod.phase",
|
||||
"k8s.daemonset.desired_scheduled_nodes",
|
||||
"k8s.daemonset.current_scheduled_nodes",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.daemonset.name", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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.pod.phase",
|
||||
"k8s.statefulset.desired_pods",
|
||||
"k8s.statefulset.current_pods",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.statefulset.name", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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.pod.phase",
|
||||
"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", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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", //k8s.node.allocatable_cpu and k8s.node.allocatable_memory are
|
||||
// controlled by allocatable_types_to_report config option (Check // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L805-L806)
|
||||
"k8s.node.condition_ready", // node counts by readiness
|
||||
"k8s.pod.phase", // pod counts per cluster by phase
|
||||
},
|
||||
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", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
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
|
||||
missingAttrs map[string]bool
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "empty bucket — nothing to emit",
|
||||
bucket: onboardingComponentBucket{Component: testComponent, DocumentationLink: testDocLink},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{},
|
||||
},
|
||||
{
|
||||
name: "all default metrics present",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: 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},
|
||||
missingAttrs: 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},
|
||||
missingAttrs: 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},
|
||||
missingAttrs: 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{},
|
||||
missingAttrs: map[string]bool{},
|
||||
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{},
|
||||
missingAttrs: map[string]bool{"a1": true},
|
||||
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},
|
||||
missingAttrs: map[string]bool{"a2": 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.missingAttrs)
|
||||
|
||||
requireMetricsEntry(t, "presentDefault", got.PresentDefault, tt.want.presentDefault)
|
||||
requireMetricsEntry(t, "presentOptional", got.PresentOptional, tt.want.presentOptional)
|
||||
requireAttrsEntry(t, "presentAttrs", got.PresentAttrs, tt.want.presentAttrs)
|
||||
|
||||
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 TestPartitionList(t *testing.T) {
|
||||
present, missing := partitionList(
|
||||
[]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 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 attributes 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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type Handler interface {
|
||||
ListStatefulSets(http.ResponseWriter, *http.Request)
|
||||
ListJobs(http.ResponseWriter, *http.Request)
|
||||
ListDaemonSets(http.ResponseWriter, *http.Request)
|
||||
GetOnboarding(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
@@ -34,4 +35,5 @@ type Module interface {
|
||||
ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (*inframonitoringtypes.StatefulSets, error)
|
||||
ListJobs(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (*inframonitoringtypes.Jobs, error)
|
||||
ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDaemonSets) (*inframonitoringtypes.DaemonSets, error)
|
||||
GetOnboarding(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableOnboarding) (*inframonitoringtypes.Onboarding, error)
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
337
tests/integration/tests/inframonitoring/11_onboarding.py
Normal file
337
tests/integration/tests/inframonitoring/11_onboarding.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""Integration tests for the v2 infra-monitoring onboarding endpoint.
|
||||
|
||||
GET /api/v2/infra_monitoring/onboarding?type=<t> reports per-tab readiness:
|
||||
for each collector component it lists which required/optional metrics and
|
||||
required attributes are present vs missing. `ready` is true iff every missing
|
||||
list is empty (optional gaps DO block).
|
||||
|
||||
Presence is checked against distributed_metadata with NO time window
|
||||
(pkg/modules/inframonitoring/implinframonitoring/helpers.go:423,:479): a metric
|
||||
is present iff it was ever ingested; an attribute is present iff it appears as a
|
||||
label on any of that type's spec metrics. So seeding here is purely "make these
|
||||
(metric, label) rows exist" — no start/end, no value math. insert_metrics is
|
||||
function-scoped and truncates metadata on teardown, so (serial suite) each test
|
||||
sees only its own seeds.
|
||||
|
||||
SPECS mirrors pkg/modules/inframonitoring/implinframonitoring/onboarding_constants.go
|
||||
and is the contract lock: if a Go spec changes, the matching assertion fails.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
|
||||
ENDPOINT = "/api/v2/infra_monitoring/onboarding"
|
||||
|
||||
# Component names (onboarding_constants.go:9-15) + their type + docs link.
|
||||
HMR = "hostmetricsreceiver"
|
||||
KSR = "kubeletstatsreceiver"
|
||||
KCR = "k8sclusterreceiver"
|
||||
RDP = "resourcedetectionprocessor"
|
||||
KAP = "k8sattributesprocessor"
|
||||
|
||||
COMPONENT_TYPE = {HMR: "receiver", KSR: "receiver", KCR: "receiver", RDP: "processor", KAP: "processor"}
|
||||
|
||||
_PODS_OPT = [
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
]
|
||||
|
||||
# Mirror of onboardingSpecs: type -> {default|optional: {component: [metrics]}, attrs: {component: [attrs]}}.
|
||||
SPECS = {
|
||||
"hosts": {
|
||||
"default": {HMR: ["system.cpu.time", "system.memory.usage", "system.cpu.load_average.15m", "system.filesystem.usage"]},
|
||||
"optional": {},
|
||||
"attrs": {RDP: ["host.name"]},
|
||||
},
|
||||
"processes": {
|
||||
"default": {HMR: ["process.cpu.time", "process.memory.usage"]},
|
||||
"optional": {},
|
||||
"attrs": {HMR: ["process.pid"]},
|
||||
},
|
||||
"pods": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.pod.uid"]},
|
||||
},
|
||||
"nodes": {
|
||||
"default": {
|
||||
KSR: ["k8s.node.cpu.usage", "k8s.node.memory.working_set"],
|
||||
KCR: ["k8s.node.allocatable_cpu", "k8s.node.allocatable_memory", "k8s.node.condition_ready", "k8s.pod.phase"],
|
||||
},
|
||||
"optional": {},
|
||||
"attrs": {KAP: ["k8s.node.name"]},
|
||||
},
|
||||
"deployments": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.deployment.desired", "k8s.deployment.available"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.deployment.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"daemonsets": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.daemonset.desired_scheduled_nodes", "k8s.daemonset.current_scheduled_nodes"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.daemonset.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"statefulsets": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.statefulset.desired_pods", "k8s.statefulset.current_pods"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.statefulset.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"jobs": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.job.desired_successful_pods", "k8s.job.active_pods", "k8s.job.failed_pods", "k8s.job.successful_pods"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.job.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"namespaces": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase"]},
|
||||
"optional": {},
|
||||
"attrs": {KAP: ["k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"clusters": {
|
||||
"default": {KSR: ["k8s.node.cpu.usage", "k8s.node.memory.working_set"], KCR: ["k8s.node.allocatable_cpu", "k8s.node.allocatable_memory", "k8s.node.condition_ready", "k8s.pod.phase"]},
|
||||
"optional": {},
|
||||
"attrs": {RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"volumes": {
|
||||
"default": {KSR: ["k8s.volume.available", "k8s.volume.capacity", "k8s.volume.inodes", "k8s.volume.inodes.free", "k8s.volume.inodes.used"]},
|
||||
"optional": {},
|
||||
"attrs": {KAP: ["k8s.persistentvolumeclaim.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
}
|
||||
|
||||
ALL_TYPES = list(SPECS.keys())
|
||||
|
||||
|
||||
# --- helpers ---
|
||||
|
||||
|
||||
def _all(d: dict) -> list:
|
||||
"""Flatten a {component: [items]} map to a flat list."""
|
||||
return [x for items in d.values() for x in items]
|
||||
|
||||
|
||||
def _all_metrics(t: str) -> list:
|
||||
return _all(SPECS[t]["default"]) + _all(SPECS[t]["optional"])
|
||||
|
||||
|
||||
def _attr_labels(t: str, drop: str | None = None) -> dict:
|
||||
"""Labels carrying every required attr (so they resolve present), minus `drop`."""
|
||||
return {a: f"v-{a}" for a in _all(SPECS[t]["attrs"]) if a != drop}
|
||||
|
||||
|
||||
# Marker label so every seeded metric registers in distributed_metadata even when
|
||||
# `labels` is empty (insert_metrics writes a metadata row per label). Non-spec, so it
|
||||
# is never counted as a present required attribute.
|
||||
_SEED_MARKER = {"test.seed.marker": "1"}
|
||||
|
||||
|
||||
def _seed(insert_metrics, metric_names: list, labels: dict) -> None:
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics([Metrics(metric_name=m, labels={**_SEED_MARKER, **labels}, timestamp=now, value=1.0) for m in metric_names])
|
||||
|
||||
|
||||
def _request(signoz: types.SigNoz, token: str, type_: str | None):
|
||||
params = {} if type_ is None else {"type": type_}
|
||||
return requests.get(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
params=params,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def _grouped(entries: list, field: str) -> dict:
|
||||
"""{component_name: set(items)} from a present/missing entry list; also asserts
|
||||
each entry's associatedComponent.type matches the known component type."""
|
||||
out: dict = {}
|
||||
for e in entries:
|
||||
comp = e["associatedComponent"]
|
||||
assert comp["type"] == COMPONENT_TYPE[comp["name"]], f"wrong type for {comp!r}"
|
||||
out.setdefault(comp["name"], set()).update(e[field])
|
||||
return out
|
||||
|
||||
|
||||
def _exp(d: dict) -> dict:
|
||||
return {comp: set(items) for comp, items in d.items()}
|
||||
|
||||
|
||||
def _check_missing_entries(entries: list) -> None:
|
||||
"""Every missing entry carries a non-empty message + a non-empty docs link
|
||||
(exact link not asserted — links are subject to change)."""
|
||||
for e in entries:
|
||||
assert e["message"], f"empty message: {e!r}"
|
||||
assert e["documentationLink"], f"empty doc link: {e!r}"
|
||||
|
||||
|
||||
# Parametrize cases derived from SPECS.
|
||||
_DEFAULT_CASES = [ # one representative dropped default metric per type
|
||||
pytest.param(t, comp, ms[0], id=f"{t}-{ms[0]}")
|
||||
for t in ALL_TYPES
|
||||
for comp, ms in [next(iter(SPECS[t]["default"].items()))]
|
||||
]
|
||||
_OPTIONAL_CASES = [ # types that have optional metrics
|
||||
pytest.param(t, comp, ms[0], id=f"{t}-{ms[0]}")
|
||||
for t in ALL_TYPES
|
||||
for comp, ms in SPECS[t]["optional"].items()
|
||||
if ms
|
||||
]
|
||||
_ATTR_CASES = [
|
||||
pytest.param(t, comp, a, id=f"{t}-{a}")
|
||||
for t in ALL_TYPES
|
||||
for comp, attrs in SPECS[t]["attrs"].items()
|
||||
for a in attrs
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_,err_substr",
|
||||
[
|
||||
pytest.param(None, "type is required", id="missing_type"),
|
||||
pytest.param("foo", "invalid type", id="invalid_type"),
|
||||
],
|
||||
)
|
||||
def test_onboarding_validation_errors(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
type_,
|
||||
err_substr: str,
|
||||
) -> None:
|
||||
"""Missing/unknown `type` query param → 400 invalid_input."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = _request(signoz, token, type_)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
error = response.json()["error"]
|
||||
assert error["code"] == "invalid_input"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_", ALL_TYPES)
|
||||
def test_onboarding_empty_backend(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics, # noqa: ARG001 ensures metadata is truncated around this test
|
||||
type_: str,
|
||||
) -> None:
|
||||
"""No data ingested → not ready; every default metric + required attr reported
|
||||
missing (bucketed by component, with message + docs link); present lists empty."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["ready"] is False
|
||||
assert data["presentDefaultEnabledMetrics"] == []
|
||||
assert data["presentOptionalMetrics"] == []
|
||||
assert data["presentRequiredAttributes"] == []
|
||||
assert _grouped(data["missingDefaultEnabledMetrics"], "metrics") == _exp(SPECS[type_]["default"])
|
||||
assert _grouped(data["missingOptionalMetrics"], "metrics") == _exp(SPECS[type_]["optional"])
|
||||
assert _grouped(data["missingRequiredAttributes"], "attributes") == _exp(SPECS[type_]["attrs"])
|
||||
_check_missing_entries(data["missingDefaultEnabledMetrics"])
|
||||
_check_missing_entries(data["missingOptionalMetrics"])
|
||||
_check_missing_entries(data["missingRequiredAttributes"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_", ALL_TYPES)
|
||||
def test_onboarding_all_present_ready(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
type_: str,
|
||||
) -> None:
|
||||
"""Every default+optional metric seeded carrying all required attrs → ready;
|
||||
present buckets exactly match the spec, all missing lists empty."""
|
||||
_seed(insert_metrics, _all_metrics(type_), _attr_labels(type_))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["type"] == type_
|
||||
assert data["ready"] is True
|
||||
assert data["missingDefaultEnabledMetrics"] == []
|
||||
assert data["missingOptionalMetrics"] == []
|
||||
assert data["missingRequiredAttributes"] == []
|
||||
assert _grouped(data["presentDefaultEnabledMetrics"], "metrics") == _exp(SPECS[type_]["default"])
|
||||
assert _grouped(data["presentOptionalMetrics"], "metrics") == _exp(SPECS[type_]["optional"])
|
||||
assert _grouped(data["presentRequiredAttributes"], "attributes") == _exp(SPECS[type_]["attrs"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_,component,metric", _DEFAULT_CASES)
|
||||
def test_onboarding_missing_default_metric(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
type_: str,
|
||||
component: str,
|
||||
metric: str,
|
||||
) -> None:
|
||||
"""One default metric never ingested (everything else present) → that metric is
|
||||
in missingDefaultEnabledMetrics under its component; not ready."""
|
||||
seed = [m for m in _all_metrics(type_) if m != metric]
|
||||
_seed(insert_metrics, seed, _attr_labels(type_))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["ready"] is False
|
||||
assert metric in _grouped(data["missingDefaultEnabledMetrics"], "metrics").get(component, set())
|
||||
assert data["missingOptionalMetrics"] == []
|
||||
assert data["missingRequiredAttributes"] == []
|
||||
_check_missing_entries(data["missingDefaultEnabledMetrics"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_,component,metric", _OPTIONAL_CASES)
|
||||
def test_onboarding_missing_optional_metric(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
type_: str,
|
||||
component: str,
|
||||
metric: str,
|
||||
) -> None:
|
||||
"""One optional metric missing → reported in missingOptionalMetrics and (locked
|
||||
decision) NOT ready, even though all default metrics + attrs are present."""
|
||||
seed = [m for m in _all_metrics(type_) if m != metric]
|
||||
_seed(insert_metrics, seed, _attr_labels(type_))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["ready"] is False
|
||||
assert metric in _grouped(data["missingOptionalMetrics"], "metrics").get(component, set())
|
||||
assert data["missingDefaultEnabledMetrics"] == []
|
||||
assert data["missingRequiredAttributes"] == []
|
||||
_check_missing_entries(data["missingOptionalMetrics"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_,component,attr", _ATTR_CASES)
|
||||
def test_onboarding_missing_required_attribute(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
type_: str,
|
||||
component: str,
|
||||
attr: str,
|
||||
) -> None:
|
||||
"""All metrics present but one required attr never seen on any of them → that
|
||||
attr is in missingRequiredAttributes under its component; not ready."""
|
||||
_seed(insert_metrics, _all_metrics(type_), _attr_labels(type_, drop=attr))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["ready"] is False
|
||||
assert attr in _grouped(data["missingRequiredAttributes"], "attributes").get(component, set())
|
||||
assert data["missingDefaultEnabledMetrics"] == []
|
||||
assert data["missingOptionalMetrics"] == []
|
||||
_check_missing_entries(data["missingRequiredAttributes"])
|
||||
Reference in New Issue
Block a user