Compare commits

..

128 Commits

Author SHA1 Message Date
nikhilmantri0902
a949993430 chore: merged main 2026-04-29 12:17:07 +05:30
swapnil-signoz
14a032119a chore: bumping cloud integration agent version to v0.0.10 (#11135)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: bumping agent version to v0.0.10

* chore: depployment
2026-04-29 05:23:22 +00:00
Yunus M
e78dfc1622 Azure service integration UI (#11117)
* feat: azure integration - ui refactor

* feat: implement AWS cloud account integration UI components and connection handling

* feat: add Azure cloud account integration UI components and connection handling

* feat: enhance Azure cloud account setup UI with prerequisites and accordion for how it works section

* feat: enhance styling for Azure and AWS account management

* refactor: clean up state initialization and destructuring in AWS and HeroSection components

* fix: update import path for ServiceDashboards in S3Sync test

* feat: add Denmark East region to Azure regions and enhance Azure account removal messaging

* chore: remove prefer-signoz-ui-icons ESLint rule and update telemetry event naming
2026-04-29 03:58:38 +00:00
nikhilmantri0902
f34a33e08b chore: merged base branch 2026-04-28 12:30:16 +05:30
Nikhil Mantri
46e833faba Merge branch 'main' into infraM/v2_pods_list_api 2026-04-28 12:15:56 +05:30
nikhilmantri0902
4bd7492629 chore: updated comment 2026-04-28 12:07:04 +05:30
Nikhil Mantri
24fe9a986d feat(infra-monitoring): v2 pods list apis - phase counts when custom grouping (#11088)
* chore: added phase counts feature

* chore: added queries for pod phase counts in custom group by

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

* chore: pod phase with local table of time series as counts

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types
2026-04-27 16:04:55 +05:30
Nikhil Mantri
56e79be6cd Merge branch 'infraM/v2_pods_list_api' into infraM/v2_onboarding_api 2026-04-27 15:05:47 +05:30
Nikhil Mantri
92d297ac9d Merge branch 'main' into infraM/v2_pods_list_api 2026-04-27 15:05:04 +05:30
Nikhil Mantri
b3c352609c Merge branch 'infraM/v2_pods_list_api' into infraM/v2_onboarding_api 2026-04-27 12:51:27 +05:30
Ashwin Bhatkal
bdbaa32485 Merge branch 'main' into infraM/v2_pods_list_api 2026-04-27 11:44:13 +05:30
nikhilmantri0902
9503cdff36 chore: added a note from otel 2026-04-25 21:42:33 +05:30
nikhilmantri0902
5a18786ab2 chore: added a note from otel 2026-04-25 21:29:41 +05:30
nikhilmantri0902
648154df14 chore: readability improvement 2026-04-25 19:30:30 +05:30
nikhilmantri0902
98eb002e07 chore: simplify 2026-04-24 18:14:48 +05:30
nikhilmantri0902
720379db9f chore: get onboarding spec 2026-04-24 17:47:03 +05:30
nikhilmantri0902
6ad14e7151 chore: renamed method 2026-04-24 17:38:56 +05:30
nikhilmantri0902
181fca064b chore: added onboarding api 2026-04-24 17:33:03 +05:30
nikhilmantri0902
a5e39ca6bd Merge branch 'infraM/v2_pods_list_api' into infraM/v2_onboarding_api 2026-04-24 17:25:27 +05:30
Ashwin Bhatkal
b35c6676f9 fix: rebase fixes 2026-04-24 13:17:02 +05:30
nikhilmantri0902
1095caa123 chore: improved api description to document -1 as no data in numeric fields 2026-04-24 12:11:45 +05:30
nikhilmantri0902
9043b49762 chore: removed pods - order by phase 2026-04-24 12:04:51 +05:30
nikhilmantri0902
d4084a7494 chore: added support for pod phase unknown 2026-04-24 11:46:26 +05:30
nikhilmantri0902
27c564b3bf chore: added required tags 2026-04-24 11:21:20 +05:30
Nikhil Mantri
f02c491828 Merge branch 'main' into infraM/v2_pods_list_api 2026-04-24 10:47:13 +05:30
Nikhil Mantri
3d53b8f77f Merge branch 'main' into infraM/v2_pods_list_api 2026-04-23 18:44:33 +05:30
nikhilmantri0902
dffe94fec4 chore: conflicts resolved 2026-04-23 18:39:39 +05:30
nikhilmantri0902
c9360fcf13 Merge branch 'infraM/v2_hosts_list_api' into infraM/v2_pods_list_api 2026-04-23 11:23:49 +05:30
nikhilmantri0902
b5ab45db20 chore: regen api client for inframonitoring
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:51:35 +05:30
Nikhil Mantri
08f76aca78 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-23 09:51:01 +05:30
nikhilmantri0902
d81cec4c29 chore: added onboarding splits 2026-04-22 19:22:37 +05:30
nikhilmantri0902
49744c6104 chore: added attrs presence check function 2026-04-22 19:06:26 +05:30
nikhilmantri0902
2147627baf chore: added specs for all component types 2026-04-22 19:00:46 +05:30
nikhilmantri0902
824f92a88f chore: added types and constants 2026-04-22 18:21:55 +05:30
nikhilmantri0902
983d4fe4f2 Merge branch 'infraM/v2_hosts_list_api' into infraM/v2_pods_list_api 2026-04-22 15:37:21 +05:30
nikhilmantri0902
833af794c3 chore: make sort stable in case of tiebreaker by comparing composite group by keys 2026-04-22 15:26:28 +05:30
nikhilmantri0902
21b51d1fcc chore: cleanup and rename 2026-04-22 15:13:00 +05:30
nikhilmantri0902
56f22682c8 Merge branch 'infraM/v2_hosts_list_api' into infraM/v2_pods_list_api 2026-04-22 14:29:17 +05:30
nikhilmantri0902
9c8359940c chore: remove a defensive nil map check, the function ensure non-nil map when err nil 2026-04-22 11:59:01 +05:30
Nikhil Mantri
4050880275 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-22 11:35:57 +05:30
nikhilmantri0902
5e775f64f2 chore: added status unauthorized 2026-04-21 21:30:44 +05:30
nikhilmantri0902
0189f23f46 chore: removed internal server error 2026-04-21 21:30:01 +05:30
nikhilmantri0902
49a36d4e3d chore: removed pod metric temporalities 2026-04-21 21:24:49 +05:30
nikhilmantri0902
9407d658ab chore: merge base hosts v2 branch 2026-04-21 21:17:28 +05:30
nikhilmantri0902
5035712485 chore: added json tag required: true 2026-04-21 18:50:25 +05:30
nikhilmantri0902
bab17c3615 chore: comments resolve 2026-04-21 18:33:56 +05:30
Nikhil Mantri
37b44f4db9 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-21 17:40:06 +05:30
nikhilmantri0902
99dd6e5f1e chore: pods code restructuring 2026-04-21 17:03:13 +05:30
nikhilmantri0902
9c7131fa6a chore: merge base branch 2026-04-21 16:22:55 +05:30
Nikhil Mantri
ad889a2e1d Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-21 13:48:53 +05:30
nikhilmantri0902
a4f6d0cbf5 chore: removed temporalities 2026-04-21 13:44:06 +05:30
nikhilmantri0902
589bed7c16 chore: comments correction 2026-04-21 12:50:51 +05:30
nikhilmantri0902
93843a1f48 chore: file structure further breakdown for clarity 2026-04-21 12:36:07 +05:30
nikhilmantri0902
88c43108fc chore: added types package 2026-04-20 18:52:43 +05:30
nikhilmantri0902
ed4cf540e8 chore: inframonitoring types renaming 2026-04-20 18:47:28 +05:30
nikhilmantri0902
9e2dfa9033 chore: rearrangement 2026-04-20 17:51:03 +05:30
nikhilmantri0902
d98d5d68ee chore: rename PodsList -> ListPods 2026-04-20 16:57:21 +05:30
nikhilmantri0902
2cb1c3b73b chore: rename HostsList -> ListHosts 2026-04-20 16:42:19 +05:30
nikhilmantri0902
ae7ca497ad chore: merged base hosts branch and reorganized code 2026-04-20 13:38:25 +05:30
Nikhil Mantri
a579916961 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-20 11:05:36 +05:30
Nikhil Mantri
4a16d56abf feat(infra-monitoring): v2 hosts list - return counts of active & inactive hosts for custom group by attributes (#10956)
* chore: add functionality for showing active and inactive counts in custom group by

* chore: bug fix

* chore: added subquery for active and total count

* chore: ignore empty string hosts in get active hosts

* fix: sinceUnixMilli for determining active hosts compute once per request

* chore: refactor code
2026-04-20 10:41:15 +05:30
Nikhil Mantri
642b5ac3f0 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-16 16:32:39 +05:30
Nikhil Mantri
a12112619c Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-16 15:41:35 +05:30
nikhilmantri0902
014785f1bc chore: ignore empty string hosts in get active hosts 2026-04-16 13:17:15 +05:30
Nikhil Mantri
58ee797b10 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-15 14:18:29 +05:30
Nikhil Mantri
82d236742f Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-15 11:21:33 +05:30
nikhilmantri0902
397e1ad5be chore: added TODOs and made filterByStatus a part of filter struct 2026-04-14 18:32:48 +05:30
nikhilmantri0902
8d6b25ca9b chore: resolved conflicts 2026-04-14 17:09:17 +05:30
nikhilmantri0902
5fa6bd8b8d Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-13 11:02:14 +05:30
nikhilmantri0902
bd9977483b chore: improved description 2026-04-11 11:31:35 +05:30
nikhilmantri0902
50fbdfeeef chore: validate order by to validate function 2026-04-10 19:01:45 +05:30
nikhilmantri0902
e2b1b73e87 chore: improvements 2026-04-10 13:23:33 +05:30
nikhilmantri0902
cb9f3fd3e5 chore: rearrage 2026-04-10 00:39:23 +05:30
nikhilmantri0902
232acc343d chore: escape backtick to prevent sql injection 2026-04-10 00:01:01 +05:30
nikhilmantri0902
2025afdccc chore: endpoint modification openapi 2026-04-09 23:25:59 +05:30
nikhilmantri0902
d2f4d4af93 chore: endpoint correction 2026-04-09 23:21:57 +05:30
Nikhil Mantri
47ff7bbb8e Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-09 23:20:39 +05:30
Nikhil Mantri
724071c5dc Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-09 18:30:15 +05:30
nikhilmantri0902
4d24979358 chore: frontend fix 2026-04-09 18:26:42 +05:30
nikhilmantri0902
042943b10a chore: distributed samples table to local table change for get metadata 2026-04-09 18:24:45 +05:30
nikhilmantri0902
48a9be7ec8 chore: added required metrics check 2026-04-09 17:38:48 +05:30
nikhilmantri0902
a9504b2120 chore: added a TODO remark 2026-04-09 16:08:34 +05:30
nikhilmantri0902
8755887c4a chore: added better metrics existence check 2026-04-09 16:01:35 +05:30
Nikhil Mantri
4cb4662b3a Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-09 15:14:25 +05:30
nikhilmantri0902
e6900dabc8 chore: warnings added passing from queryResponse warning to host lists response struct 2026-04-09 00:09:38 +05:30
nikhilmantri0902
c1ba389b63 chore: add type for response and files rearrange 2026-04-08 23:35:53 +05:30
nikhilmantri0902
3a1f40234f Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-08 23:03:50 +05:30
Nikhil Mantri
2e4891fa63 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-08 16:07:57 +05:30
Nikhil Mantri
04ebc0bec7 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-08 11:08:10 +05:30
nikhilmantri0902
271f9b81ed Merge branch 'infraM/v2_hosts_list_api' into infraM/v2_pods_list_api 2026-04-07 21:55:47 +05:30
nikhilmantri0902
6fa815c294 chore: modified getMetadata query 2026-04-07 18:55:57 +05:30
nikhilmantri0902
63ec518efb chore: added hostName logic 2026-04-07 17:36:15 +05:30
nikhilmantri0902
c4ca20dd90 chore: return errors from getMetadata and lint fix 2026-04-07 17:01:13 +05:30
nikhilmantri0902
e56cc4222b chore: return errors from getMetadata and lint fix 2026-04-07 16:57:35 +05:30
nikhilmantri0902
07d2944d7c chore: yarn generate api 2026-04-07 16:44:06 +05:30
nikhilmantri0902
dea01ae36a chore: hostStatusNone added for clarity that this field can be left empty as well in payload 2026-04-07 16:32:25 +05:30
nikhilmantri0902
62ea5b54e2 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-07 14:09:48 +05:30
nikhilmantri0902
e549a7e42f chore: added pods list api updates 2026-04-07 13:58:10 +05:30
nikhilmantri0902
90e2ebb11f Merge branch 'infraM/v2_hosts_list_api' into infraM/v2_pods_list_api 2026-04-07 13:51:35 +05:30
nikhilmantri0902
61baa1be7a chore: code improvements 2026-04-07 13:49:00 +05:30
nikhilmantri0902
b946fa665f Merge branch 'infraM/v2_hosts_list_api' into infraM/v2_pods_list_api 2026-04-07 11:15:35 +05:30
nikhilmantri0902
2e049556e4 chore: unified composite key function 2026-04-07 11:15:03 +05:30
nikhilmantri0902
492a5e70d7 chore: added pods metrics temporality 2026-04-06 17:33:44 +05:30
nikhilmantri0902
ba1f2771e8 Merge branch 'infraM/v2_hosts_list_api' into infraM/v2_pods_list_api 2026-04-06 17:18:44 +05:30
nikhilmantri0902
7458fb4855 Merge branch 'main' into infraM/v2_hosts_list_api 2026-04-06 17:18:01 +05:30
nikhilmantri0902
5f55f3938b chore: added temporalities of metrics 2026-04-06 17:17:15 +05:30
nikhilmantri0902
3e8102485c Merge branch 'infraM/v2_hosts_list_api' into infraM/v2_pods_list_api 2026-04-04 20:52:50 +05:30
nikhilmantri0902
861c682ea5 chore: nil pointer dereference fix in req.Filter 2026-04-04 20:52:08 +05:30
nikhilmantri0902
c8e5895dff chore: nil pointer check 2026-04-04 20:45:04 +05:30
nikhilmantri0902
82d72e7edb chore: pods api meta start time 2026-04-04 17:18:04 +05:30
nikhilmantri0902
a3f8ecaaf1 chore: merged base branch 2026-04-04 16:47:10 +05:30
nikhilmantri0902
19aada656c chore: updated spec 2026-04-04 16:44:15 +05:30
nikhilmantri0902
b21bb4280f chore: updated openapi yml 2026-04-04 16:38:22 +05:30
nikhilmantri0902
bc0a4fdb5c chore: added pods list logic 2026-04-04 13:24:46 +05:30
nikhilmantri0902
37fb0e9254 Merge branch 'infraM/base_dependencies' into infraM/v2_hosts_list_api 2026-04-03 17:49:00 +05:30
nikhilmantri0902
aecfa1a174 chore: added validation on order by 2026-04-02 20:13:30 +05:30
nikhilmantri0902
b869d23d94 chore: moved funcs 2026-04-02 20:02:22 +05:30
nikhilmantri0902
6ee3d44f76 chore: removed isSendingK8sAgentsMetricsCode 2026-04-02 19:58:30 +05:30
nikhilmantri0902
462e554107 chore: yarn generate api 2026-04-02 14:49:15 +05:30
nikhilmantri0902
66afa73e6f chore: return status as a string 2026-04-02 14:39:02 +05:30
nikhilmantri0902
54c604bcf4 chore: added some unit tests 2026-04-02 14:20:27 +05:30
nikhilmantri0902
c1be02ba54 chore: added validate function 2026-04-02 14:14:34 +05:30
nikhilmantri0902
d3c7ba8f45 chore: disk usage 2026-04-02 14:01:18 +05:30
nikhilmantri0902
039c4a0496 fix: bug fix 2026-04-02 11:32:49 +05:30
nikhilmantri0902
51a94b6bbc chore: added logic for hosts v3 api 2026-04-02 02:52:28 +05:30
nikhilmantri0902
bbfbb94f52 chore: merged main 2026-04-01 00:45:40 +05:30
nikhilmantri0902
d1eb9ef16f chore: endpoint detail update 2026-03-31 16:16:31 +05:30
nikhilmantri0902
3db00f8bc3 chore: baseline setup 2026-03-31 15:27:18 +05:30
83 changed files with 3104 additions and 1689 deletions

View File

@@ -2103,8 +2103,6 @@ components:
type: boolean
org_id:
type: string
source:
type: string
updatedAt:
format: date-time
type: string
@@ -2136,7 +2134,6 @@ components:
type: object
DashboardtypesStorableDashboardData:
additionalProperties: {}
nullable: true
type: object
DashboardtypesUpdatablePublicDashboard:
properties:
@@ -10058,186 +10055,6 @@ paths:
summary: Update a span mapper
tags:
- spanmapper
/api/v1/system/{source}:
get:
deprecated: false
description: This endpoint returns the system dashboard for the callers org
keyed by source (e.g. ai-o11y-overview).
operationId: GetSystemDashboard
parameters:
- in: path
name: source
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboard'
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 system dashboard
tags:
- systemdashboard
put:
deprecated: false
description: This endpoint replaces the system dashboard for the callers org
with the provided payload.
operationId: UpdateSystemDashboard
parameters:
- in: path
name: source
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardtypesStorableDashboardData'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboard'
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:
- EDITOR
- tokenizer:
- EDITOR
summary: Update system dashboard
tags:
- systemdashboard
/api/v1/system/{source}/reset:
post:
deprecated: false
description: This resets edited/updated system dashboard to default system dashboard.
operationId: ResetSystemDashboard
parameters:
- in: path
name: source
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboard'
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:
- EDITOR
- tokenizer:
- EDITOR
summary: Reset system dashboard to defaults
tags:
- systemdashboard
/api/v1/testChannel:
post:
deprecated: true

View File

@@ -2781,10 +2781,6 @@ export interface DashboardtypesDashboardDTO {
* @type string
*/
org_id?: string;
/**
* @type string
*/
source?: string;
/**
* @type string
* @format date-time
@@ -2827,12 +2823,9 @@ export interface DashboardtypesPostablePublicDashboardDTO {
timeRangeEnabled?: boolean;
}
/**
* @nullable
*/
export type DashboardtypesStorableDashboardDataDTO = {
export interface DashboardtypesStorableDashboardDataDTO {
[key: string]: unknown;
} | null;
}
export interface DashboardtypesUpdatablePublicDashboardDTO {
/**
@@ -7342,39 +7335,6 @@ export type UpdateSpanMapperPathParameters = {
groupId: string;
mapperId: string;
};
export type GetSystemDashboardPathParameters = {
source: string;
};
export type GetSystemDashboard200 = {
data: DashboardtypesDashboardDTO;
/**
* @type string
*/
status: string;
};
export type UpdateSystemDashboardPathParameters = {
source: string;
};
export type UpdateSystemDashboard200 = {
data: DashboardtypesDashboardDTO;
/**
* @type string
*/
status: string;
};
export type ResetSystemDashboardPathParameters = {
source: string;
};
export type ResetSystemDashboard200 = {
data: DashboardtypesDashboardDTO;
/**
* @type string
*/
status: string;
};
export type ListUsersDeprecated200 = {
/**
* @type array

View File

@@ -1,318 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
DashboardtypesStorableDashboardDataDTO,
GetSystemDashboard200,
GetSystemDashboardPathParameters,
RenderErrorResponseDTO,
ResetSystemDashboard200,
ResetSystemDashboardPathParameters,
UpdateSystemDashboard200,
UpdateSystemDashboardPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint returns the system dashboard for the callers org keyed by source (e.g. ai-o11y-overview).
* @summary Get system dashboard
*/
export const getSystemDashboard = (
{ source }: GetSystemDashboardPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetSystemDashboard200>({
url: `/api/v1/system/${source}`,
method: 'GET',
signal,
});
};
export const getGetSystemDashboardQueryKey = ({
source,
}: GetSystemDashboardPathParameters) => {
return [`/api/v1/system/${source}`] as const;
};
export const getGetSystemDashboardQueryOptions = <
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ source }: GetSystemDashboardPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetSystemDashboardQueryKey({ source });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getSystemDashboard>>
> = ({ signal }) => getSystemDashboard({ source }, signal);
return {
queryKey,
queryFn,
enabled: !!source,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetSystemDashboardQueryResult = NonNullable<
Awaited<ReturnType<typeof getSystemDashboard>>
>;
export type GetSystemDashboardQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get system dashboard
*/
export function useGetSystemDashboard<
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ source }: GetSystemDashboardPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetSystemDashboardQueryOptions({ source }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get system dashboard
*/
export const invalidateGetSystemDashboard = async (
queryClient: QueryClient,
{ source }: GetSystemDashboardPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetSystemDashboardQueryKey({ source }) },
options,
);
return queryClient;
};
/**
* This endpoint replaces the system dashboard for the callers org with the provided payload.
* @summary Update system dashboard
*/
export const updateSystemDashboard = (
{ source }: UpdateSystemDashboardPathParameters,
dashboardtypesStorableDashboardDataDTO: BodyType<DashboardtypesStorableDashboardDataDTO>,
) => {
return GeneratedAPIInstance<UpdateSystemDashboard200>({
url: `/api/v1/system/${source}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesStorableDashboardDataDTO,
});
};
export const getUpdateSystemDashboardMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSystemDashboard>>,
TError,
{
pathParams: UpdateSystemDashboardPathParameters;
data: BodyType<DashboardtypesStorableDashboardDataDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateSystemDashboard>>,
TError,
{
pathParams: UpdateSystemDashboardPathParameters;
data: BodyType<DashboardtypesStorableDashboardDataDTO>;
},
TContext
> => {
const mutationKey = ['updateSystemDashboard'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateSystemDashboard>>,
{
pathParams: UpdateSystemDashboardPathParameters;
data: BodyType<DashboardtypesStorableDashboardDataDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateSystemDashboard(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateSystemDashboardMutationResult = NonNullable<
Awaited<ReturnType<typeof updateSystemDashboard>>
>;
export type UpdateSystemDashboardMutationBody =
BodyType<DashboardtypesStorableDashboardDataDTO>;
export type UpdateSystemDashboardMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update system dashboard
*/
export const useUpdateSystemDashboard = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSystemDashboard>>,
TError,
{
pathParams: UpdateSystemDashboardPathParameters;
data: BodyType<DashboardtypesStorableDashboardDataDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateSystemDashboard>>,
TError,
{
pathParams: UpdateSystemDashboardPathParameters;
data: BodyType<DashboardtypesStorableDashboardDataDTO>;
},
TContext
> => {
const mutationOptions = getUpdateSystemDashboardMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This resets edited/updated system dashboard to default system dashboard.
* @summary Reset system dashboard to defaults
*/
export const resetSystemDashboard = (
{ source }: ResetSystemDashboardPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ResetSystemDashboard200>({
url: `/api/v1/system/${source}/reset`,
method: 'POST',
signal,
});
};
export const getResetSystemDashboardMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetSystemDashboard>>,
TError,
{ pathParams: ResetSystemDashboardPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof resetSystemDashboard>>,
TError,
{ pathParams: ResetSystemDashboardPathParameters },
TContext
> => {
const mutationKey = ['resetSystemDashboard'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof resetSystemDashboard>>,
{ pathParams: ResetSystemDashboardPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return resetSystemDashboard(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type ResetSystemDashboardMutationResult = NonNullable<
Awaited<ReturnType<typeof resetSystemDashboard>>
>;
export type ResetSystemDashboardMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Reset system dashboard to defaults
*/
export const useResetSystemDashboard = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetSystemDashboard>>,
TError,
{ pathParams: ResetSystemDashboardPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof resetSystemDashboard>>,
TError,
{ pathParams: ResetSystemDashboardPathParameters },
TContext
> => {
const mutationOptions = getResetSystemDashboardMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -0,0 +1,22 @@
.codeBlock {
position: relative;
}
.codeBlockSyntaxHighlighter {
background-color: var(--l2-background) !important;
border-radius: 4px !important;
border: 1px solid var(--l2-border) !important;
color: var(--l2-foreground) !important;
pre {
color: var(--l2-foreground) !important;
font-family: 'Geist Mono' !important;
font-size: 12px !important;
}
code {
color: var(--l1-foreground) !important;
font-family: 'Geist Mono' !important;
font-size: 12px !important;
}
}

View File

@@ -0,0 +1,46 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import CodeBlock from './CodeBlock';
const mockCopyToClipboard = jest.fn();
jest.mock('react-use', () => ({
useCopyToClipboard: (): [unknown, (text: string) => void] => [
undefined,
mockCopyToClipboard,
],
}));
describe('CodeBlock', () => {
beforeEach(() => {
mockCopyToClipboard.mockReset();
});
it('renders code block mode by default', () => {
render(<CodeBlock code={'const x = 1;\n'} language="javascript" />);
const container = screen.getByTestId('code-block-container');
expect(container).toBeInTheDocument();
expect(container).toHaveTextContent('const x = 1;');
});
it('renders inline code when inline is true', () => {
render(<CodeBlock code="inline value" inline />);
const inlineCode = screen.getByText('inline value');
expect(inlineCode.tagName.toLowerCase()).toBe('code');
expect(screen.queryByTestId('code-block-container')).not.toBeInTheDocument();
});
it('copies code and triggers callback', async () => {
const onCopy = jest.fn();
render(<CodeBlock code="SELECT * FROM logs;" onCopy={onCopy} />);
fireEvent.click(screen.getByRole('button', { name: /copy code/i }));
await waitFor(() => {
expect(mockCopyToClipboard).toHaveBeenCalledWith('SELECT * FROM logs;');
});
expect(onCopy).toHaveBeenCalledWith('SELECT * FROM logs;');
});
});

View File

@@ -0,0 +1,89 @@
import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Check, Copy } from '@signozhq/icons';
import { Button } from '@signozhq/ui';
import SyntaxHighlighter, {
a11yDark,
} from 'components/MarkdownRenderer/syntaxHighlighter';
import styles from './CodeBlock.module.scss';
export interface CodeBlockProps {
code: string;
language?: string;
className?: string;
inline?: boolean;
showLineNumbers?: boolean;
showCopyButton?: boolean;
onCopy?: (copiedCode: string) => void;
}
function CodeBlock({
code,
language = 'text',
className,
inline = false,
showLineNumbers = false,
showCopyButton = true,
onCopy,
}: CodeBlockProps): JSX.Element {
const [isCopied, setIsCopied] = useState(false);
const [, copyToClipboard] = useCopyToClipboard();
const normalizedCode = useMemo(() => code?.replace(/\n$/, '') ?? '', [code]);
const handleCopy = (): void => {
copyToClipboard(normalizedCode);
setIsCopied(true);
onCopy?.(normalizedCode);
setTimeout(() => {
setIsCopied(false);
}, 1000);
};
if (inline) {
return <code className={className}>{normalizedCode}</code>;
}
return (
<div
className={`${styles.codeBlock} ${className}`}
style={{ position: 'relative' }}
data-testid="code-block-container"
>
{showCopyButton ? (
<Button
variant="ghost"
color="secondary"
size="sm"
onClick={handleCopy}
prefix={isCopied ? <Check size={14} /> : <Copy size={14} />}
aria-label="Copy code"
title={isCopied ? 'Copied' : 'Copy'}
style={{ position: 'absolute', right: 8, top: 8, zIndex: 1 }}
/>
) : null}
<SyntaxHighlighter
style={a11yDark}
language={language}
PreTag="div"
showLineNumbers={showLineNumbers}
wrapLongLines
className={styles.codeBlockSyntaxHighlighter}
>
{normalizedCode}
</SyntaxHighlighter>
</div>
);
}
CodeBlock.defaultProps = {
language: 'text',
className: undefined,
inline: false,
showLineNumbers: false,
showCopyButton: true,
onCopy: undefined,
};
export default CodeBlock;

View File

@@ -1,8 +1,8 @@
import { Color } from '@signozhq/design-tokens';
import { Alert, Spin } from 'antd';
import { LoaderCircle, TriangleAlert } from 'lucide-react';
import { Callout } from '@signozhq/ui';
import { Spin } from 'antd';
import { LoaderCircle } from 'lucide-react';
import { ModalStateEnum } from '../types';
import { ModalStateEnum } from '../HeroSection/types';
function AlertMessage({
modalState,
@@ -12,14 +12,13 @@ function AlertMessage({
switch (modalState) {
case ModalStateEnum.WAITING:
return (
<Alert
message={
<Callout
title={
<div className="cloud-account-setup-form__alert-message">
<Spin
indicator={
<LoaderCircle
size={14}
color={Color.BG_AMBER_400}
className="anticon anticon-loading anticon-spin ant-spin-dot"
/>
}
@@ -28,21 +27,19 @@ function AlertMessage({
<span className="retry-time">10</span> secs...
</div>
}
className="cloud-account-setup-form__alert"
type="warning"
type="info"
showIcon={false}
/>
);
case ModalStateEnum.ERROR:
return (
<Alert
message={
<Callout
title={
<div className="cloud-account-setup-form__alert-message">
<TriangleAlert type="solid" size={15} color={Color.BG_SAKURA_400} />
{`We couldn't establish a connection to your AWS account. Please try again`}
</div>
}
type="error"
className="cloud-account-setup-form__alert"
/>
);
default:

View File

@@ -117,6 +117,12 @@
min-width: 140px !important;
}
&.azure {
.ant-select-selector {
min-width: 282px !important;
}
}
.ant-select-item-option-active {
background: var(--l3-background) !important;
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui';
@@ -6,19 +6,29 @@ import { Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { useListAccounts } from 'api/generated/services/cloudintegration';
import cx from 'classnames';
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import {
CloudAccount as IntegrationCloudAccount,
IntegrationType,
} from 'container/Integrations/types';
import useUrlQuery from 'hooks/useUrlQuery';
import { ChevronDown, Dot, PencilLine, Plug, Plus } from 'lucide-react';
import { mapAccountDtoToAwsCloudAccount } from '../../mapAwsCloudAccountFromDto';
import { CloudAccount } from '../../types';
import AccountSettingsModal from './AccountSettingsModal';
import CloudAccountSetupModal from './CloudAccountSetupModal';
import AzureCloudAccountSetupModal from '../../AzureCloudServices/AddNewAccount/CloudAccountSetupModal';
import AzureAccountSettingsModal from '../../AzureCloudServices/EditAccount/AccountSettingsModal';
import {
mapAccountDtoToAwsCloudAccount,
mapAccountDtoToAzureCloudAccount,
} from '../../mapCloudAccountFromDto';
import AwsCloudAccountSetupModal from '../AddNewAccount/CloudAccountSetupModal';
import AwsAccountSettingsModal from '../EditAccount/AccountSettingsModal';
import { CloudAccount as AwsCloudAccount } from '../types';
import './AccountActions.style.scss';
function AccountActionsRenderer({
type,
accounts,
isLoading,
activeAccount,
@@ -27,9 +37,10 @@ function AccountActionsRenderer({
onIntegrationModalOpen,
onAccountSettingsModalOpen,
}: {
accounts: CloudAccount[] | undefined;
type: IntegrationType;
accounts: IntegrationCloudAccount[] | undefined;
isLoading: boolean;
activeAccount: CloudAccount | null;
activeAccount: IntegrationCloudAccount | null;
selectOptions: SelectProps['options'];
onAccountChange: (value: string) => void;
onIntegrationModalOpen: () => void;
@@ -57,9 +68,11 @@ function AccountActionsRenderer({
<Select
value={activeAccount?.providerAccountId}
options={selectOptions}
rootClassName="cloud-account-selector"
rootClassName={cx('cloud-account-selector', {
[type.toLowerCase()]: type,
})}
popupMatchSelectWidth={false}
placeholder="Select AWS Account"
placeholder={`Select ${type} Account`}
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
onChange={onAccountChange}
/>
@@ -102,21 +115,49 @@ function AccountActionsRenderer({
);
}
function AccountActions(): JSX.Element {
function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: listAccountsResponse, isLoading } = useListAccounts({
cloudProvider: INTEGRATION_TYPES.AWS,
cloudProvider: type,
});
const accounts = useMemo((): CloudAccount[] | undefined => {
const accounts = useMemo((): IntegrationCloudAccount[] | undefined => {
const raw = listAccountsResponse?.data?.accounts;
if (!raw) {
return undefined;
}
return raw
.map(mapAccountDtoToAwsCloudAccount)
.filter((account): account is CloudAccount => account !== null);
}, [listAccountsResponse]);
const mappedAccounts: IntegrationCloudAccount[] = [];
if (type === IntegrationType.AWS_SERVICES) {
raw.forEach((account) => {
if (!account) {
return;
}
const mapped = mapAccountDtoToAwsCloudAccount(account);
if (mapped) {
mappedAccounts.push(mapped);
}
});
}
if (type === IntegrationType.AZURE_SERVICES) {
raw.forEach((account) => {
if (!account) {
return;
}
const mapped = mapAccountDtoToAzureCloudAccount(account);
if (mapped) {
mappedAccounts.push(mapped);
}
});
}
return mappedAccounts;
}, [listAccountsResponse, type]);
const initialAccount = useMemo(
() =>
@@ -127,9 +168,8 @@ function AccountActions(): JSX.Element {
[accounts, urlQuery],
);
const [activeAccount, setActiveAccount] = useState<CloudAccount | null>(
initialAccount,
);
const [activeAccount, setActiveAccount] =
useState<IntegrationCloudAccount | null>(initialAccount);
// Update state when initial value changes
useEffect(() => {
@@ -149,16 +189,17 @@ function AccountActions(): JSX.Element {
}, [initialAccount]);
const [isIntegrationModalOpen, setIsIntegrationModalOpen] = useState(false);
const startAccountConnectionAttempt = (): void => {
setIsIntegrationModalOpen(true);
logEvent('AWS Integration: Account connection attempt started', {});
logEvent(`${type} Integration: Account connection attempt started`, {});
};
const [isAccountSettingsModalOpen, setIsAccountSettingsModalOpen] =
useState(false);
const openAccountSettings = (): void => {
setIsAccountSettingsModalOpen(true);
logEvent('AWS Integration: Account settings viewed', {
logEvent(`${type} Integration: Account settings viewed`, {
cloudAccountId: activeAccount?.cloud_account_id,
});
};
@@ -166,13 +207,16 @@ function AccountActions(): JSX.Element {
// log telemetry event when an account is viewed.
useEffect(() => {
if (activeAccount) {
logEvent('AWS Integration: Account viewed', {
logEvent(`${type} Integration: Account viewed`, {
cloudAccountId: activeAccount?.cloud_account_id,
status: activeAccount?.status,
enabledRegions: activeAccount?.config?.regions,
enabledRegions:
'regions' in activeAccount.config
? activeAccount.config.regions
: activeAccount.config.resource_groups,
});
}
}, [activeAccount]);
}, [activeAccount, type]);
const selectOptions: SelectProps['options'] = useMemo(
() =>
@@ -188,6 +232,7 @@ function AccountActions(): JSX.Element {
return (
<div className="hero-section__actions">
<AccountActionsRenderer
type={type}
accounts={accounts}
isLoading={isLoading}
activeAccount={activeAccount}
@@ -204,17 +249,39 @@ function AccountActions(): JSX.Element {
/>
{isIntegrationModalOpen && (
<CloudAccountSetupModal
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
<>
{type === IntegrationType.AWS_SERVICES && (
<AwsCloudAccountSetupModal
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
)}
{type === IntegrationType.AZURE_SERVICES && (
<AzureCloudAccountSetupModal
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
)}
</>
)}
{isAccountSettingsModalOpen && activeAccount && (
<AccountSettingsModal
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount}
setActiveAccount={setActiveAccount}
/>
<>
{type === IntegrationType.AWS_SERVICES && (
<AwsAccountSettingsModal
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as AwsCloudAccount}
setActiveAccount={
setActiveAccount as Dispatch<SetStateAction<AwsCloudAccount | null>>
}
/>
)}
{type === IntegrationType.AZURE_SERVICES && (
<AzureAccountSettingsModal
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount}
setActiveAccount={setActiveAccount}
/>
)}
</>
)}
</div>
);

View File

@@ -0,0 +1,346 @@
.cloud-account-setup-modal {
background: var(--l1-background);
color: var(--l1-foreground);
[data-slot='drawer-title'] {
color: var(--l1-foreground);
}
> div {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__content {
flex: 1;
overflow-y: auto;
min-height: 0;
scrollbar-width: thin;
padding-right: 16px;
&::-webkit-scrollbar-thumb {
background: var(--l1-border);
}
&::-webkit-scrollbar-track {
background: var(--l1-background);
}
scrollbar-width: thin;
scrollbar-color: var(--l3-background) var(--l1-background);
}
.cloud-account-setup-prerequisites {
display: flex;
flex-direction: column;
gap: 12px;
&__title {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
&__list {
display: flex;
flex-direction: column;
gap: 8px;
list-style: none;
padding: 0;
margin: 0;
}
&__list-item {
color: var(--l2-foreground);
font-size: 13px;
line-height: 18px;
letter-spacing: -0.06px;
display: flex;
align-items: center;
gap: 16px;
&-bullet {
color: var(--primary);
font-weight: 500;
}
&-text {
display: flex;
align-items: center;
gap: 4px;
}
}
&__list-item-highlight {
color: var(--l1-foreground);
font-weight: 500;
}
}
.cloud-account-setup-how-it-works-accordion {
display: flex;
flex-direction: column;
margin: 24px 0;
&__title {
display: flex;
align-items: center;
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
border-radius: 4px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
padding: 4px 16px 4px 0px;
&.open {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
&__description {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
opacity: 0;
transform: translateY(-8px);
animation: cloud-account-setup-accordion-reveal 220ms ease-out forwards;
border-radius: 4px;
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
border: 1px solid var(--l2-border);
background: var(--l2-background);
&-item {
display: flex;
flex-direction: column;
gap: 8px;
color: var(--l1-foreground);
font-size: 13px;
line-height: 18px;
letter-spacing: -0.06px;
}
@media (prefers-reduced-motion: reduce) {
animation: none;
opacity: 1;
transform: none;
}
}
}
.cloud-account-setup-form__code-block-tabs {
padding: 8px;
border-radius: 4px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
&-header {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 12px;
&-title {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
&-description {
color: var(--l2-foreground);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
}
[role='tablist'] {
gap: 8px !important;
}
[role='tabpanel'] {
padding: 0 !important;
}
[data-slot='tabs-trigger'] {
padding: 4px 24px !important;
border: none !important;
background-color: transparent !important;
font-size: 12px !important;
}
}
@keyframes cloud-account-setup-accordion-reveal {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.account-setup-modal-footer {
&__confirm-button {
background: var(--primary-background);
color: var(--primary-foreground);
font-size: 12px;
font-weight: 500;
}
&__confirm-selection-count {
font-family: 'Geist Mono';
}
&__close-button {
background: var(--l1-background);
border: 1px solid var(--l1-border);
border-radius: 2px;
color: var(--l1-foreground);
font-family: 'Inter';
font-size: 12px;
font-weight: 500;
&:hover {
border-color: var(--l1-border);
color: var(--l1-foreground);
}
}
}
.cloud-account-setup-form {
.disabled {
opacity: 0.4;
}
&,
&__content {
display: flex;
flex-direction: column;
gap: 24px;
}
&__alert {
width: 100%;
[data-slot='callout'] {
width: 100%;
box-sizing: border-box;
}
&-message {
display: flex;
align-items: center;
gap: 8px;
color: var(--l1-foreground);
.retry-time {
font-family: 'Geist Mono';
font-size: 14px;
font-weight: 600;
line-height: 22px;
letter-spacing: -0.07px;
}
}
}
&__form-group {
display: flex;
flex-direction: column;
gap: 12px;
}
&__title {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
&__description {
color: var(--l2-foreground);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
&__select {
.ant-select-selection-item {
color: var(--l1-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
}
&__form-item {
margin: 0;
}
&__include-all-regions-switch {
display: flex;
align-items: center;
gap: 10px;
color: var(--l2-foreground);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
margin-bottom: 12px;
&-label {
background-color: transparent;
border: none;
color: var(--l2-foreground);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
cursor: pointer;
}
}
&__note {
padding: 12px;
color: var(--callout-primary-description);
font-size: 12px;
line-height: 22px;
letter-spacing: -0.06px;
border-radius: 4px;
border: 1px solid
color-mix(in srgb, var(--primary-background) 10%, transparent);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
}
&__submit-button {
border-radius: 2px;
background: var(--primary-background);
color: var(--primary-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
&-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
&:disabled {
opacity: 0.4;
}
}
}
}

View File

@@ -8,8 +8,8 @@ import {
ActiveViewEnum,
IntegrationModalProps,
ModalStateEnum,
} from '../types';
import { RegionForm } from './RegionForm';
} from '../../../HeroSection/types';
import { RegionForm } from '../RegionForm/RegionForm';
import './CloudAccountSetupModal.style.scss';
@@ -74,8 +74,6 @@ function CloudAccountSetupModal({
isConnectionParamsLoading,
setSelectedRegions,
setIncludeAllRegions,
isLoading,
isGeneratingUrl,
handleConnectionSuccess,
handleConnectionTimeout,
handleConnectionError,

View File

@@ -9,10 +9,10 @@ import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Save } from 'lucide-react';
import logEvent from '../../../../../../api/common/logEvent';
import { CloudAccount } from '../../types';
import { RegionSelector } from './RegionSelector';
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
import logEvent from '../../../../../api/common/logEvent';
import RemoveIntegrationAccount from '../../RemoveAccount/RemoveIntegrationAccount';
import { RegionSelector } from '../RegionForm/RegionSelector';
import { CloudAccount } from '../types';
import './AccountSettingsModal.style.scss';
@@ -110,11 +110,7 @@ function AccountSettingsModal({
form,
selectedRegions,
includeAllRegions,
account?.id,
handleRemoveIntegrationAccountSuccess,
isSaveDisabled,
handleSubmit,
isLoading,
account?.providerAccountId,
setSelectedRegions,
setIncludeAllRegions,
]);
@@ -133,6 +129,7 @@ function AccountSettingsModal({
<RemoveIntegrationAccount
accountId={account?.id}
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
cloudProvider={INTEGRATION_TYPES.AWS}
/>
<Button

View File

@@ -1,28 +0,0 @@
import awsDarkLogoUrl from '@/assets/Logos/aws-dark.svg';
import AccountActions from './components/AccountActions';
import './HeroSection.style.scss';
function HeroSection(): JSX.Element {
return (
<div className="hero-section">
<div className="hero-section__details">
<div className="hero-section__details-header">
<div className="hero-section__icon">
<img src={awsDarkLogoUrl} alt="AWS" />
</div>
<div className="hero-section__details-title">AWS</div>
</div>
<div className="hero-section__details-description">
AWS is a cloud computing platform that provides a range of services for
building and running applications.
</div>
</div>
<AccountActions />
</div>
);
}
export default HeroSection;

View File

@@ -1,180 +0,0 @@
.cloud-account-setup-modal {
background: var(--l1-background);
color: var(--l1-foreground);
[data-slot='drawer-title'] {
color: var(--l1-foreground);
}
> div {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__content {
flex: 1;
overflow-y: auto;
min-height: 0;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 2px;
}
}
.account-setup-modal-footer {
&__confirm-button {
background: var(--primary-background);
color: var(--primary-foreground);
font-size: 12px;
font-weight: 500;
}
&__confirm-selection-count {
font-family: 'Geist Mono';
}
&__close-button {
background: var(--l1-background);
border: 1px solid var(--l1-border);
border-radius: 2px;
color: var(--l1-foreground);
font-family: 'Inter';
font-size: 12px;
font-weight: 500;
&:hover {
border-color: var(--l1-border);
color: var(--l1-foreground);
}
}
}
.cloud-account-setup-form {
.disabled {
opacity: 0.4;
}
&,
&__content {
display: flex;
flex-direction: column;
gap: 24px;
}
&__alert {
&.ant-alert {
padding: 12px;
border-radius: 6px;
font-size: 14px;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
&.ant-alert-error {
color: var(--danger-foreground);
border: 1px solid
color-mix(in srgb, var(--danger-background) 10%, transparent);
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
}
&.ant-alert-warning {
color: var(--warning-foreground);
border: 1px solid
color-mix(in srgb, var(--warning-background) 10%, transparent);
background: color-mix(in srgb, var(--warning-background) 10%, transparent);
}
&-message {
display: flex;
align-items: center;
gap: 8px;
color: var(--l1-foreground);
.retry-time {
font-family: 'Geist Mono';
font-size: 14px;
font-weight: 600;
line-height: 22px;
letter-spacing: -0.07px;
}
}
}
&__form-group {
display: flex;
flex-direction: column;
gap: 12px;
}
&__title {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
&__description {
color: var(--l2-foreground);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
&__select {
.ant-select-selection-item {
color: var(--l1-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
}
&__form-item {
margin: 0;
}
&__include-all-regions-switch {
display: flex;
align-items: center;
gap: 10px;
color: var(--l2-foreground);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
margin-bottom: 12px;
&-label {
background-color: transparent;
border: none;
color: var(--l2-foreground);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
cursor: pointer;
}
}
&__note {
padding: 12px;
color: var(--callout-primary-description);
font-size: 12px;
line-height: 22px;
letter-spacing: -0.06px;
border-radius: 4px;
border: 1px solid
color-mix(in srgb, var(--primary-background) 10%, transparent);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
}
&__submit-button {
border-radius: 2px;
background: var(--primary-background);
color: var(--primary-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
&-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
&:disabled {
opacity: 0.4;
}
}
}
}

View File

@@ -5,7 +5,7 @@ import { ChevronDown } from 'lucide-react';
import { Region } from 'utils/regions';
import { popupContainer } from 'utils/selectPopupContainer';
import { RegionSelector } from './RegionSelector';
import { RegionSelector } from './RegionForm/RegionSelector';
// Form section components
function RegionDeploymentSection({

View File

@@ -3,15 +3,18 @@ import { Form } from 'antd';
import { useGetAccount } from 'api/generated/services/cloudintegration';
import cx from 'classnames';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import {
ModalStateEnum,
RegionFormProps,
} from 'container/Integrations/HeroSection/types';
import { regions } from 'utils/regions';
import { ModalStateEnum, RegionFormProps } from '../types';
import AlertMessage from './AlertMessage';
import AlertMessage from '../../AlertMessage';
import {
ComplianceNote,
MonitoringRegionsSection,
RegionDeploymentSection,
} from './IntegrateNowFormSections';
} from '../IntegrateNowFormSections';
import RenderConnectionFields from './RenderConnectionParams';
export function RegionForm({
@@ -76,8 +79,6 @@ export function RegionForm({
layout="vertical"
onFinish={onSubmit}
>
<AlertMessage modalState={modalState} />
<div
className={cx(`cloud-account-setup-form__content`, {
disabled: isFormDisabled,
@@ -100,6 +101,10 @@ export function RegionForm({
isFormDisabled={isFormDisabled}
/>
</div>
<div className="cloud-account-setup-form__alert">
<AlertMessage modalState={modalState} />
</div>
</Form>
);
}

View File

@@ -4,7 +4,7 @@ import { useListAccounts } from 'api/generated/services/cloudintegration';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import useUrlQuery from 'hooks/useUrlQuery';
import { mapAccountDtoToAwsCloudAccount } from '../mapAwsCloudAccountFromDto';
import { mapAccountDtoToAwsCloudAccount } from '../../mapCloudAccountFromDto';
import { CloudAccount } from '../types';
import './S3BucketsSelector.styles.scss';

View File

@@ -12,14 +12,14 @@ import {
useUpdateService,
} from 'api/generated/services/cloudintegration';
import {
CloudintegrationtypesServiceConfigDTO,
CloudintegrationtypesServiceDTO,
ListServicesMetadata200,
} from 'api/generated/services/sigNoz.schemas';
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import ServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { IServiceStatus } from 'container/Integrations/types';
import ServiceDashboards from 'container/Integrations/CloudIntegration/ServiceDashboards/ServiceDashboards';
import { IntegrationType, IServiceStatus } from 'container/Integrations/types';
import useUrlQuery from 'hooks/useUrlQuery';
import { Save, X } from 'lucide-react';
@@ -36,7 +36,81 @@ type ServiceDetailsData = CloudintegrationtypesServiceDTO & {
status?: IServiceStatus;
};
function ServiceDetails(): JSX.Element | null {
const EMPTY_FORM_VALUES: ServiceConfigFormValues = {
logsEnabled: false,
metricsEnabled: false,
s3BucketsByRegion: {},
};
function getInitialFormValues(
type: IntegrationType,
serviceDetailsData?: ServiceDetailsData,
): ServiceConfigFormValues {
const integrationConfig =
type === IntegrationType.AWS_SERVICES
? serviceDetailsData?.cloudIntegrationService?.config?.aws
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
return {
logsEnabled: integrationConfig?.logs?.enabled || false,
metricsEnabled: integrationConfig?.metrics?.enabled || false,
s3BucketsByRegion:
type === IntegrationType.AWS_SERVICES
? serviceDetailsData?.cloudIntegrationService?.config?.aws?.logs
?.s3Buckets || {}
: {},
};
}
function getServiceConfigPayload({
type,
serviceId,
logsEnabled,
metricsEnabled,
isLogsSupported,
isMetricsSupported,
s3BucketsByRegion,
}: {
type: IntegrationType;
serviceId: string;
logsEnabled: boolean;
metricsEnabled: boolean;
isLogsSupported: boolean;
isMetricsSupported: boolean;
s3BucketsByRegion: Record<string, string[]>;
}): CloudintegrationtypesServiceConfigDTO {
if (type === IntegrationType.AWS_SERVICES) {
return {
aws: {
logs: {
enabled: isLogsSupported ? logsEnabled : false,
s3Buckets:
serviceId === 's3sync' && isLogsSupported ? s3BucketsByRegion : {},
},
metrics: {
enabled: isMetricsSupported ? metricsEnabled : false,
},
},
};
}
return {
azure: {
logs: {
enabled: isLogsSupported ? logsEnabled : false,
},
metrics: {
enabled: isMetricsSupported ? metricsEnabled : false,
},
},
};
}
function ServiceDetails({
type,
}: {
type: IntegrationType;
}): JSX.Element | null {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId');
const serviceId = urlQuery.get('service');
@@ -51,7 +125,7 @@ function ServiceDetails(): JSX.Element | null {
isLoading: isServiceDetailsLoading,
} = useGetService(
{
cloudProvider: INTEGRATION_TYPES.AWS,
cloudProvider: type,
serviceId: serviceId || '',
},
{
@@ -65,10 +139,17 @@ function ServiceDetails(): JSX.Element | null {
},
);
const awsConfig = serviceDetailsData?.cloudIntegrationService?.config?.aws;
const integrationConfig =
type === IntegrationType.AWS_SERVICES
? serviceDetailsData?.cloudIntegrationService?.config?.aws
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
const isServiceEnabledInPersistedConfig =
Boolean(awsConfig?.logs?.enabled) || Boolean(awsConfig?.metrics?.enabled);
Boolean(integrationConfig?.logs?.enabled) ||
Boolean(integrationConfig?.metrics?.enabled);
const serviceDetailsId = serviceDetailsData?.id;
const isLogsSupported = serviceDetailsData?.supportedSignals?.logs || false;
const isMetricsSupported =
serviceDetailsData?.supportedSignals?.metrics || false;
const {
control,
@@ -77,43 +158,31 @@ function ServiceDetails(): JSX.Element | null {
watch,
formState: { isDirty },
} = useForm<ServiceConfigFormValues>({
defaultValues: {
logsEnabled: awsConfig?.logs?.enabled || false,
metricsEnabled: awsConfig?.metrics?.enabled || false,
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
},
defaultValues: getInitialFormValues(type, serviceDetailsData),
});
const resetToAwsConfig = useCallback((): void => {
reset({
logsEnabled: awsConfig?.logs?.enabled || false,
metricsEnabled: awsConfig?.metrics?.enabled || false,
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
});
}, [awsConfig, reset]);
const resetToConfig = useCallback((): void => {
reset(getInitialFormValues(type, serviceDetailsData));
}, [reset, serviceDetailsData, type]);
// Ensure form state does not leak across service switches while new details load.
useEffect(() => {
reset({
logsEnabled: false,
metricsEnabled: false,
s3BucketsByRegion: {},
});
reset(EMPTY_FORM_VALUES);
}, [reset, serviceId]);
useEffect(() => {
resetToAwsConfig();
}, [resetToAwsConfig, serviceDetailsId]);
resetToConfig();
}, [resetToConfig, serviceDetailsId]);
// log telemetry event on visiting details of a service.
useEffect(() => {
if (serviceId) {
logEvent('AWS Integration: Service viewed', {
logEvent(`${type} Integration: Service viewed`, {
cloudAccountId,
serviceId,
});
}
}, [cloudAccountId, serviceId]);
}, [cloudAccountId, serviceId, type]);
const { mutate: updateService, isLoading: isUpdatingServiceConfig } =
useUpdateService();
@@ -121,8 +190,8 @@ function ServiceDetails(): JSX.Element | null {
const queryClient = useQueryClient();
const handleDiscard = useCallback((): void => {
resetToAwsConfig();
}, [resetToAwsConfig]);
resetToConfig();
}, [resetToConfig]);
const onSubmit = useCallback(
async (values: ServiceConfigFormValues): Promise<void> => {
@@ -141,25 +210,25 @@ function ServiceDetails(): JSX.Element | null {
return;
}
const serviceConfigPayload = getServiceConfigPayload({
type,
serviceId,
logsEnabled,
metricsEnabled,
isLogsSupported,
isMetricsSupported,
s3BucketsByRegion: normalizedS3BucketsByRegion,
});
updateService(
{
pathParams: {
cloudProvider: INTEGRATION_TYPES.AWS,
cloudProvider: type,
id: cloudAccountId,
serviceId,
},
data: {
config: {
aws: {
logs: {
enabled: logsEnabled,
s3Buckets: normalizedS3BucketsByRegion,
},
metrics: {
enabled: metricsEnabled,
},
},
},
config: serviceConfigPayload,
},
},
{
@@ -170,7 +239,7 @@ function ServiceDetails(): JSX.Element | null {
const servicesListQueryKey = getListServicesMetadataQueryKey(
{
cloudProvider: INTEGRATION_TYPES.AWS,
cloudProvider: type,
},
{
cloud_integration_id: cloudAccountId,
@@ -203,7 +272,7 @@ function ServiceDetails(): JSX.Element | null {
invalidateGetService(
queryClient,
{
cloudProvider: INTEGRATION_TYPES.AWS,
cloudProvider: type,
serviceId,
},
{
@@ -214,14 +283,14 @@ function ServiceDetails(): JSX.Element | null {
invalidateListServicesMetadata(
queryClient,
{
cloudProvider: INTEGRATION_TYPES.AWS,
cloudProvider: type,
},
{
cloud_integration_id: cloudAccountId,
},
);
logEvent('AWS Integration: Service settings saved', {
logEvent(`${type} Integration: Service settings saved`, {
cloudAccountId,
serviceId,
logsEnabled,
@@ -241,7 +310,16 @@ function ServiceDetails(): JSX.Element | null {
console.error('Form submission failed:', error);
}
},
[serviceId, cloudAccountId, updateService, queryClient, reset],
[
serviceId,
cloudAccountId,
updateService,
queryClient,
reset,
type,
isLogsSupported,
isMetricsSupported,
],
);
if (isServiceDetailsLoading) {
@@ -262,10 +340,6 @@ function ServiceDetails(): JSX.Element | null {
const logsEnabled = watch('logsEnabled');
const s3BucketsByRegion = watch('s3BucketsByRegion');
const isLogsSupported = serviceDetailsData?.supportedSignals?.logs || false;
const isMetricsSupported =
serviceDetailsData?.supportedSignals?.metrics || false;
const hasUnsavedChanges = isDirty;
const isS3SyncBucketsMissing =

View File

@@ -1,29 +0,0 @@
import useUrlQuery from 'hooks/useUrlQuery';
import HeroSection from './HeroSection/HeroSection';
import ServiceDetails from './ServiceDetails/ServiceDetails';
import ServicesList from './ServicesList';
import './ServicesTabs.style.scss';
function ServicesTabs(): JSX.Element {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
return (
<div className="services-tabs">
<HeroSection />
<div className="services-section">
<div className="services-section__sidebar">
<ServicesList cloudAccountId={cloudAccountId} />
</div>
<div className="services-section__content">
<ServiceDetails />
</div>
</div>
</div>
);
}
export default ServicesTabs;

View File

@@ -29,7 +29,7 @@ jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
MarkdownRenderer: (): JSX.Element => <div data-testid="markdown-renderer" />,
}));
jest.mock(
'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards',
'container/Integrations/CloudIntegration/ServiceDashboards/ServiceDashboards',
() => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="service-dashboards" />,

View File

@@ -1,4 +1,5 @@
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import { IntegrationType } from 'container/Integrations/types';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import ServiceDetails from '../ServiceDetails/ServiceDetails';
@@ -11,10 +12,11 @@ import { accountsResponse } from './mockData';
const renderServiceDetails = (
_initialConfigLogsS3Buckets: Record<string, string[]> = {},
_serviceId = 's3sync',
type: IntegrationType = IntegrationType.AWS_SERVICES,
): RenderResult =>
render(
<MockQueryClientProvider>
<ServiceDetails />
<ServiceDetails type={type} />
</MockQueryClientProvider>,
);

View File

@@ -1,25 +0,0 @@
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
import { CloudAccount } from './types';
export function mapAccountDtoToAwsCloudAccount(
account: CloudintegrationtypesAccountDTO,
): CloudAccount | null {
if (!account.providerAccountId) {
return null;
}
return {
id: account.id,
cloud_account_id: account.id,
config: {
regions: account.config?.aws?.regions ?? [],
},
status: {
integration: {
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
},
},
providerAccountId: account.providerAccountId,
};
}

View File

@@ -0,0 +1,356 @@
import { useCallback, useRef, useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import { Button, Callout, DrawerWrapper, Tabs } from '@signozhq/ui';
import { Form, Select, Spin } from 'antd';
import { useGetAccount } from 'api/generated/services/cloudintegration';
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
import CodeBlock from 'components/CodeBlock/CodeBlock';
import {
AZURE_REGIONS,
INTEGRATION_TYPES,
} from 'container/Integrations/constants';
import {
IntegrationModalProps,
ModalStateEnum,
} from 'container/Integrations/HeroSection/types';
import { LoaderCircle, SquareArrowOutUpRight } from 'lucide-react';
import { popupContainer } from 'utils/selectPopupContainer';
import { useIntegrationModal } from '../../../../../hooks/integration/azure/useIntegrationModal';
import RenderConnectionFields from '../../AmazonWebServices/RegionForm/RenderConnectionParams';
import '../../AmazonWebServices/AddNewAccount/CloudAccountSetupModal.style.scss';
const AZURE_CLI_DESC =
'Paste the following command if you have Azure CLI setup locally on your machine or use BASH CloudShell on Azure portal with above mentioned permissions.';
const AZURE_POWERSHELL_DESC =
'Paste the following command in PowerShell CloudShell on Azure portal, you can switch to PowerShell on Azure portal.';
function CloudAccountSetupModal({
onClose,
}: IntegrationModalProps): JSX.Element {
const {
form,
modalState,
isLoading,
accountId,
connectionCommands,
handleSubmit,
handleClose,
connectionParams,
isConnectionParamsLoading,
handleConnectionSuccess,
handleConnectionTimeout,
handleConnectionError,
} = useIntegrationModal({ onClose });
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
const errorTimeout = 10 * 60 * 1000;
const [isHowItWorksOpen, setIsHowItWorksOpen] = useState(true);
const [activeTab, setActiveTab] = useState('cli');
useGetAccount(
{
cloudProvider: INTEGRATION_TYPES.AZURE,
id: accountId ?? '',
},
{
query: {
enabled: Boolean(accountId) && modalState === ModalStateEnum.WAITING,
refetchInterval,
select: (response): CloudintegrationtypesAccountDTO => response.data,
onSuccess: (account) => {
const isConnected =
Boolean(account.providerAccountId) && account.removedAt === null;
if (isConnected) {
handleConnectionSuccess({
cloudAccountId: account.providerAccountId ?? account.id,
status: account.agentReport,
});
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
handleConnectionTimeout({ id: accountId });
}
},
onError: () => {
handleConnectionError();
},
},
},
);
const renderAlert = useCallback((): JSX.Element | null => {
if (modalState === ModalStateEnum.WAITING) {
return (
<div className="cloud-account-setup-form__alert">
<Callout
title={
<div className="cloud-account-setup-form__alert-message">
<Spin
indicator={
<LoaderCircle
size={14}
className="anticon anticon-loading anticon-spin ant-spin-dot"
/>
}
/>
Waiting for Azure account connection, retrying in{' '}
<span className="retry-time">10</span> secs...
</div>
}
type="info"
showIcon={false}
/>
</div>
);
}
if (modalState === ModalStateEnum.ERROR) {
return (
<div className="cloud-account-setup-form__alert">
<Callout
title={
<div className="cloud-account-setup-form__alert-message">
We couldn&apos;t establish a connection to your Azure account. Please
try again
</div>
}
type="error"
/>
</div>
);
}
return null;
}, [modalState]);
const footer = (
<div className="cloud-account-setup-modal__footer">
{modalState === ModalStateEnum.FORM && (
<Button
variant="solid"
color="primary"
prefix={<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />}
onClick={handleSubmit}
loading={isLoading}
>
Generate Azure Setup Commands
</Button>
)}
</div>
);
return (
<DrawerWrapper
open={true}
className="cloud-account-setup-modal"
onOpenChange={(open): void => {
if (!open) {
handleClose();
}
}}
direction="right"
showCloseButton
title="Add Azure Account"
width="wide"
footer={footer}
>
<div className="cloud-account-setup-modal__content">
<div className="cloud-account-setup-prerequisites">
<div className="cloud-account-setup-prerequisites__title">
Prerequisites
</div>
<ul className="cloud-account-setup-prerequisites__list">
<li className="cloud-account-setup-prerequisites__list-item">
<span className="cloud-account-setup-prerequisites__list-item-bullet">
</span>{' '}
<span className="cloud-account-setup-prerequisites__list-item-text">
Ensure that you&apos;re logged in to the Azure workspace which you want
to monitor.
</span>
</li>
<li className="cloud-account-setup-prerequisites__list-item">
<span className="cloud-account-setup-prerequisites__list-item-bullet">
</span>{' '}
<span className="cloud-account-setup-prerequisites__list-item-text">
Ensure that you either have the{' '}
<span className="cloud-account-setup-prerequisites__list-item-highlight">
Owner
</span>{' '}
role OR
</span>
</li>
<li className="cloud-account-setup-prerequisites__list-item">
<span className="cloud-account-setup-prerequisites__list-item-bullet">
</span>{' '}
<span className="cloud-account-setup-prerequisites__list-item-text">
Both the{' '}
<span className="cloud-account-setup-prerequisites__list-item-highlight">
Contributor
</span>{' '}
and{' '}
<span className="cloud-account-setup-prerequisites__list-item-highlight">
user access admin
</span>{' '}
roles
</span>
</li>
</ul>
</div>
<div className="cloud-account-setup-how-it-works-accordion">
<div
className={`cloud-account-setup-how-it-works-accordion__title ${
isHowItWorksOpen ? 'open' : ''
}`}
>
<Button
variant="link"
color="secondary"
onClick={(): void => setIsHowItWorksOpen(!isHowItWorksOpen)}
prefix={isHowItWorksOpen ? <ChevronDown /> : <ChevronRight />}
/>
<span className="cloud-account-setup-how-it-works-accordion__title-text">
How it works?
</span>
</div>
{isHowItWorksOpen && (
<div className="cloud-account-setup-how-it-works-accordion__description">
<div className="cloud-account-setup-how-it-works-accordion__description-item">
SigNoz will create new resource-group to manage the resources required
for this integration. The following steps will create a User-Assigned
Managed Identity with the necessary permissions and follows the
Principle of Least Privilege.
</div>
<div className="cloud-account-setup-how-it-works__description-item">
Once the Integration template is deployed, you can enable the services
you want to monitor right here in Signoz dashboard.
</div>
</div>
)}
</div>
<Form
form={form}
className="cloud-account-setup-form"
layout="vertical"
initialValues={{ resourceGroups: [] }}
>
<div className="cloud-account-setup-form__content">
<div className="cloud-account-setup-form__form-group">
<div className="cloud-account-setup-form__title">
Where should we deploy the SigNoz collector resources?
</div>
<div className="cloud-account-setup-form__description">
Choose the Azure region for deployment.
</div>
<Form.Item
name="region"
rules={[{ required: true, message: 'Please select a region' }]}
className="cloud-account-setup-form__form-item"
>
<Select
placeholder="e.g. East US"
options={AZURE_REGIONS.map((region) => ({
label: `${region.label} (${region.value})`,
value: region.value,
}))}
getPopupContainer={popupContainer}
disabled={modalState === ModalStateEnum.WAITING}
/>
</Form.Item>
</div>
<div className="cloud-account-setup-form__form-group">
<div className="cloud-account-setup-form__title">
Which resource groups do you want to monitor?
</div>
<div className="cloud-account-setup-form__description">
Add one or more Azure resource group names.
</div>
<Form.Item
name="resourceGroups"
rules={[
{
required: true,
type: 'array',
min: 1,
message: 'Please add at least one resource group',
},
]}
className="cloud-account-setup-form__form-item"
>
<Select
mode="tags"
placeholder="e.g. prod-platform-rg"
tokenSeparators={[',']}
disabled={modalState === ModalStateEnum.WAITING}
/>
</Form.Item>
</div>
<RenderConnectionFields
isConnectionParamsLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
isFormDisabled={modalState === ModalStateEnum.WAITING}
/>
{connectionCommands && (
<div className="cloud-account-setup-form__code-block-tabs-container">
<div className="cloud-account-setup-form__code-block-tabs-header">
<div className="cloud-account-setup-form__code-block-tabs-header-title">
Deploy Agent
</div>
<div className="cloud-account-setup-form__code-block-tabs-header-description">
{activeTab === 'cli' ? AZURE_CLI_DESC : AZURE_POWERSHELL_DESC}
</div>
</div>
<Tabs
className="cloud-account-setup-form__code-block-tabs"
items={[
{
key: 'cli',
label: 'CLI',
children: <CodeBlock code={connectionCommands?.cliCommand || ''} />,
},
{
key: 'powershell',
label: 'PowerShell',
children: (
<CodeBlock
code={connectionCommands?.cloudPowerShellCommand || ''}
/>
),
},
]}
value={activeTab}
onChange={(key): void => setActiveTab(key)}
variant="primary"
/>
</div>
)}
{renderAlert()}
{modalState === ModalStateEnum.WAITING && (
<div className="cloud-account-setup-status-message">
After running the command, return here and wait for automatic connection
detection.
</div>
)}
</div>
</Form>
</div>
</DrawerWrapper>
);
}
export default CloudAccountSetupModal;

View File

@@ -0,0 +1,150 @@
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useQueryClient } from 'react-query';
import { Button, DrawerWrapper } from '@signozhq/ui';
import { Form, Select } from 'antd';
import { invalidateListAccounts } from 'api/generated/services/cloudintegration';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { CloudAccount } from 'container/Integrations/types';
import { Save } from 'lucide-react';
import { useAccountSettingsModal } from '../../../../../hooks/integration/azure/useAccountSettingsModal';
import RemoveIntegrationAccount from '../../RemoveAccount/RemoveIntegrationAccount';
import '../../AmazonWebServices/EditAccount/AccountSettingsModal.style.scss';
interface AccountSettingsModalProps {
onClose: () => void;
account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
}
function AccountSettingsModal({
onClose,
account,
setActiveAccount,
}: AccountSettingsModalProps): JSX.Element {
const {
form,
isLoading,
resourceGroups,
isSaveDisabled,
setResourceGroups,
handleSubmit,
handleClose,
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
const queryClient = useQueryClient();
const azureConfig = useMemo(
() => ('deployment_region' in account.config ? account.config : null),
[account.config],
);
return (
<DrawerWrapper
open={true}
className="account-settings-modal"
title="Account Settings"
direction="right"
showCloseButton
onOpenChange={(open): void => {
if (!open) {
handleClose();
}
}}
width="wide"
footer={
<div className="account-settings-modal__footer">
<RemoveIntegrationAccount
accountId={account?.id}
onRemoveIntegrationAccountSuccess={(): void => {
void invalidateListAccounts(queryClient, {
cloudProvider: INTEGRATION_TYPES.AZURE,
});
setActiveAccount(null);
handleClose();
}}
cloudProvider={INTEGRATION_TYPES.AZURE}
/>
<Button
variant="solid"
color="secondary"
disabled={isSaveDisabled}
onClick={handleSubmit}
loading={isLoading}
prefix={<Save size={14} />}
>
Update Changes
</Button>
</div>
}
>
<Form
form={form}
layout="vertical"
initialValues={{
resourceGroups: azureConfig?.resource_groups || [],
}}
>
<div className="account-settings-modal__body">
<div className="account-settings-modal__body-account-info">
<div className="account-settings-modal__body-account-info-connected-account-details">
<div className="account-settings-modal__body-account-info-connected-account-details-title">
Connected Account details
</div>
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
Azure Subscription:{' '}
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
{account?.providerAccountId}
</span>
</div>
</div>
</div>
{azureConfig?.deployment_region && (
<div className="account-settings-modal__body-region-selector">
<div className="account-settings-modal__body-region-selector-title">
Deployment region
</div>
<div className="account-settings-modal__body-region-selector-description">
{azureConfig.deployment_region}
</div>
</div>
)}
<div className="account-settings-modal__body-region-selector">
<div className="account-settings-modal__body-region-selector-title">
Resource groups
</div>
<div className="account-settings-modal__body-region-selector-description">
Update the resource groups that should be monitored.
</div>
<Form.Item
name="resourceGroups"
rules={[
{
required: true,
type: 'array',
min: 1,
message: 'Please add at least one resource group',
},
]}
>
<Select
mode="tags"
value={resourceGroups}
onChange={(values): void => {
setResourceGroups(values);
form.setFieldValue('resourceGroups', values);
}}
/>
</Form.Item>
</div>
</div>
</Form>
</DrawerWrapper>
);
}
export default AccountSettingsModal;

View File

@@ -1,16 +1,15 @@
import { IntegrationType } from 'container/Integrations/types';
import AWSTabs from './AmazonWebServices/ServicesTabs';
import Header from './Header/Header';
import ServicesTabs from './ServiceTabs/ServicesTabs';
import './CloudIntegration.styles.scss';
const CloudIntegration = ({ type }: { type: IntegrationType }): JSX.Element => {
return (
<div className="cloud-integration-container">
<Header title={type} />
{type === IntegrationType.AWS_SERVICES && <AWSTabs />}
<Header type={type} />
<ServicesTabs type={type} />
</div>
);
};

View File

@@ -7,7 +7,7 @@ import { Blocks, LifeBuoy } from 'lucide-react';
import './Header.styles.scss';
function Header({ title }: { title: IntegrationType }): JSX.Element {
function Header({ type }: { type: IntegrationType }): JSX.Element {
return (
<div className="cloud-header">
<div className="cloud-header__navigation">
@@ -25,27 +25,30 @@ function Header({ title }: { title: IntegrationType }): JSX.Element {
),
},
{
title: <div className="cloud-header__breadcrumb-title">{title}</div>,
title: <div className="cloud-header__breadcrumb-title">{type}</div>,
},
]}
/>
</div>
<div className="cloud-header__actions">
<Button
variant="solid"
size="sm"
color="secondary"
onClick={(): void => {
window.open(
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
'_blank',
);
}}
prefix={<LifeBuoy size={12} />}
>
Get Help
</Button>
</div>
{type === IntegrationType.AWS_SERVICES && (
<div className="cloud-header__actions">
<Button
variant="solid"
size="sm"
color="secondary"
onClick={(): void => {
window.open(
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
'_blank',
);
}}
prefix={<LifeBuoy size={12} />}
>
Get Help
</Button>
</div>
)}
</div>
);
}

View File

@@ -1,9 +1,16 @@
.remove-integration-account-modal {
&__cloud-provider {
color: var(--l1-foreground);
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
.ant-modal-content {
background-color: var(--l1-background);
border: 1px solid var(--l3-background);
border-radius: 4px;
padding: 12px;
}
.ant-modal-close {

View File

@@ -4,16 +4,21 @@ import { Modal } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { useDisconnectAccount } from 'api/generated/services/cloudintegration';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
import {
INTEGRATION_TELEMETRY_EVENTS,
INTEGRATION_TYPES,
} from 'container/Integrations/constants';
import { useNotifications } from 'hooks/useNotifications';
import { Unlink } from 'lucide-react';
import './RemoveIntegrationAccount.scss';
function RemoveIntegrationAccount({
cloudProvider,
accountId,
onRemoveIntegrationAccountSuccess,
}: {
cloudProvider: string;
accountId: string;
onRemoveIntegrationAccountSuccess: () => void;
}): JSX.Element {
@@ -39,12 +44,13 @@ function RemoveIntegrationAccount({
},
});
const handleOk = (): void => {
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATION_ACCOUNT_REMOVED, {
accountId,
integration: cloudProvider,
});
disconnectAccount({
pathParams: {
cloudProvider: 'aws',
cloudProvider,
id: accountId,
},
});
@@ -78,13 +84,28 @@ function RemoveIntegrationAccount({
loading: isRemoveIntegrationLoading,
}}
>
Removing this account will remove all components created for sending
telemetry to SigNoz in your AWS account within the next ~15 minutes
(cloudformation stacks named signoz-integration-telemetry-collection in
enabled regions). <br />
<br />
After that, you can delete the cloudformation stack that was created
manually when connecting this account.
{cloudProvider === INTEGRATION_TYPES.AWS ? (
<>
Removing this account will remove all components created for sending
telemetry to SigNoz in your AWS account within the next ~15 minutes
(cloudformation stacks named signoz-integration-telemetry-collection in
enabled regions). <br />
<br />
After that, you can delete the cloudformation stack that was created
manually when connecting this account.
</>
) : (
<>
Removing this account will remove all components created for sending
telemetry to SigNoz in your Azure subscription within the next ~15 minutes
(deployment stack named signoz-integration-telemetry will be deleted
automatically). <br />
<br />
After that, you have to manually delete &apos;signoz-integration&apos;
deployment stack that was created while connecting this account (Takes ~20
minutes to delete).
</>
)}
</Modal>
</div>
);

View File

@@ -1,7 +1,7 @@
import cx from 'classnames';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { Service } from './types';
import { Service } from './AmazonWebServices/types';
function ServiceItem({
service,

View File

@@ -0,0 +1,30 @@
import { IntegrationType } from 'container/Integrations/types';
import useUrlQuery from 'hooks/useUrlQuery';
import HeroSection from '../../HeroSection/HeroSection';
import ServiceDetails from '../AmazonWebServices/ServiceDetails/ServiceDetails';
import ServicesList from '../ServicesList';
import './ServicesTabs.style.scss';
function ServicesTabs({ type }: { type: IntegrationType }): JSX.Element {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
return (
<div className="services-tabs">
<HeroSection type={type} />
<div className="services-section">
<div className="services-section__sidebar">
<ServicesList cloudAccountId={cloudAccountId} type={type} />
</div>
<div className="services-section__content">
<ServiceDetails type={type} />
</div>
</div>
</div>
);
}
export default ServicesTabs;

View File

@@ -4,15 +4,20 @@ import { Skeleton } from 'antd';
import { useListServicesMetadata } from 'api/generated/services/cloudintegration';
import type { CloudintegrationtypesServiceMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import { IntegrationType } from 'container/Integrations/types';
import useUrlQuery from 'hooks/useUrlQuery';
import emptyStateIconUrl from '@/assets/Icons/emptyState.svg';
interface ServicesListProps {
cloudAccountId: string;
type: IntegrationType;
}
function ServicesList({ cloudAccountId }: ServicesListProps): JSX.Element {
function ServicesList({
cloudAccountId,
type,
}: ServicesListProps): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const hasValidCloudAccountId = Boolean(cloudAccountId);
@@ -22,7 +27,7 @@ function ServicesList({ cloudAccountId }: ServicesListProps): JSX.Element {
const { data: servicesMetadata, isLoading } = useListServicesMetadata(
{
cloudProvider: 'aws',
cloudProvider: type,
},
serviceQueryParams,
);

View File

@@ -0,0 +1,49 @@
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
import { CloudAccount as IntegrationCloudAccount } from 'container/Integrations/types';
import { CloudAccount as AwsCloudAccount } from './AmazonWebServices/types';
export function mapAccountDtoToAwsCloudAccount(
account: CloudintegrationtypesAccountDTO,
): AwsCloudAccount | null {
if (!account.providerAccountId) {
return null;
}
return {
id: account.id,
cloud_account_id: account.id,
config: {
regions: account.config?.aws?.regions ?? [],
},
status: {
integration: {
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
},
},
providerAccountId: account.providerAccountId,
};
}
export function mapAccountDtoToAzureCloudAccount(
account: CloudintegrationtypesAccountDTO,
): IntegrationCloudAccount | null {
if (!account.providerAccountId) {
return null;
}
return {
id: account.id,
cloud_account_id: account.id,
config: {
deployment_region: account.config?.azure?.deploymentRegion ?? '',
resource_groups: account.config?.azure?.resourceGroups ?? [],
},
status: {
integration: {
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
},
},
providerAccountId: account.providerAccountId,
};
}

View File

@@ -1,5 +1,32 @@
import { ONE_CLICK_INTEGRATIONS } from '../constants';
import { IntegrationType } from '../types';
export const getAccountById = <T extends { cloud_account_id: string }>(
accounts: T[],
accountId: string,
): T | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;
interface IntegrationMetadata {
title: string;
description: string;
logo: string;
}
export const getIntegrationMetadata = (
type: IntegrationType,
): IntegrationMetadata => {
const integration = ONE_CLICK_INTEGRATIONS.find(
(integration) => integration.id === type,
);
if (!integration) {
return { title: '', description: '', logo: '' };
}
return {
title: integration.title,
description: integration.description,
logo: integration.icon,
};
};

View File

@@ -0,0 +1,33 @@
import { IntegrationType } from 'container/Integrations/types';
import AccountActions from '../CloudIntegration/AmazonWebServices/AccountActions/AccountActions';
import { getIntegrationMetadata } from '../CloudIntegration/utils';
import './HeroSection.style.scss';
function HeroSection({ type }: { type: IntegrationType }): JSX.Element {
const {
title,
description,
logo: integrationLogo,
} = getIntegrationMetadata(type);
return (
<div className="hero-section">
<div className="hero-section__details">
<div className="hero-section__details-header">
<div className="hero-section__icon">
<img src={integrationLogo} alt={type} />
</div>
<div className="hero-section__details-title">{title}</div>
</div>
<div className="hero-section__details-description">{description}</div>
</div>
<AccountActions type={type} />
</div>
);
}
export default HeroSection;

View File

@@ -9,53 +9,6 @@
flex-direction: column;
gap: 16px;
.error-container {
display: flex;
border-radius: 6px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
align-items: center;
justify-content: center;
flex-direction: column;
.error-content {
display: flex;
flex-direction: column;
justify-content: center;
height: 300px;
gap: 15px;
.error-btns {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
.retry-btn {
display: flex;
align-items: center;
}
.contact-support {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
.text {
color: var(--callout-primary-description);
font-weight: 500;
}
}
}
.error-state-svg {
height: 40px;
width: 40px;
}
}
}
.loading-integration-details {
display: flex;
flex-direction: column;
@@ -327,6 +280,36 @@
}
}
}
.error-container {
display: flex;
border-radius: 6px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
align-items: center;
justify-content: center;
flex-direction: column;
.error-content {
display: flex;
flex-direction: column;
justify-content: center;
height: 300px;
gap: 15px;
.error-btns {
display: flex;
flex-direction: row;
gap: 12px;
align-items: center;
}
.error-state-svg {
height: 40px;
width: 40px;
}
}
}
}
.remove-integration-modal {

View File

@@ -1,6 +1,5 @@
import { useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui';
import { Flex, Skeleton, Typography } from 'antd';
import ROUTES from 'constants/routes';
@@ -55,8 +54,19 @@ function IntegrationDetailPage(): JSX.Element {
),
);
if (integrationId === INTEGRATION_TYPES.AWS) {
return <CloudIntegration type={IntegrationType.AWS_SERVICES} />;
if (
integrationId === INTEGRATION_TYPES.AWS ||
integrationId === INTEGRATION_TYPES.AZURE
) {
return (
<CloudIntegration
type={
integrationId === INTEGRATION_TYPES.AWS
? IntegrationType.AWS_SERVICES
: IntegrationType.AZURE_SERVICES
}
/>
);
}
return (
@@ -85,20 +95,20 @@ function IntegrationDetailPage(): JSX.Element {
<div className="error-btns">
<Button
variant="solid"
color="primary"
color="secondary"
onClick={(): Promise<any> => refetch()}
prefix={<RotateCw size={14} />}
>
Retry
</Button>
<div
className="contact-support"
<Button
variant="solid"
color="secondary"
onClick={(): void => handleContactSupport(isCloudUserVal)}
suffix={<MoveUpRight size={12} />}
>
<Typography.Link className="text">Contact Support </Typography.Link>
<MoveUpRight size={14} color={Color.BG_ROBIN_400} />
</div>
Contact Support
</Button>
</div>
</div>
</div>

View File

@@ -22,6 +22,7 @@ function OneClickIntegrations(props: OneClickIntegrationsProps): JSX.Element {
if (!query) {
return ONE_CLICK_INTEGRATIONS;
}
return ONE_CLICK_INTEGRATIONS.filter(
(integration) =>
integration.title.toLowerCase().includes(query) ||

View File

@@ -14,8 +14,8 @@ export const INTEGRATION_TELEMETRY_EVENTS = {
'Integrations Detail Page: Clicked remove Integration button for integration',
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
'Integrations Detail Page: Navigated to configure an integration',
AWS_INTEGRATION_ACCOUNT_REMOVED:
'AWS Integration Detail page: Clicked remove Integration button for integration',
INTEGRATION_ACCOUNT_REMOVED:
'Integration Detail page: Clicked remove Integration button for integration',
};
export const INTEGRATION_TYPES = {
@@ -53,7 +53,7 @@ export const AZURE_INTEGRATION = {
is_new: true,
};
export const ONE_CLICK_INTEGRATIONS = [AWS_INTEGRATION];
export const ONE_CLICK_INTEGRATIONS = [AWS_INTEGRATION, AZURE_INTEGRATION];
export const AZURE_REGIONS: AzureRegion[] = [
{
@@ -81,6 +81,7 @@ export const AZURE_REGIONS: AzureRegion[] = [
{ label: 'Central India', value: 'centralindia', geography: 'India' },
{ label: 'Central US', value: 'centralus', geography: 'United States' },
{ label: 'Chile Central', value: 'chilecentral', geography: 'Chile' },
{ label: 'Denmark East', value: 'denmarkeast', geography: 'Denmark' },
{ label: 'East Asia', value: 'eastasia', geography: 'Asia Pacific' },
{ label: 'East US', value: 'eastus', geography: 'United States' },
{ label: 'East US 2', value: 'eastus2', geography: 'United States' },

View File

@@ -4,8 +4,8 @@ import {
} from './CloudIntegration/AmazonWebServices/types';
export enum IntegrationType {
AWS_SERVICES = 'aws-services',
AZURE_SERVICES = 'azure-services',
AWS_SERVICES = 'aws',
AZURE_SERVICES = 'azure',
}
interface LogField {
@@ -89,6 +89,7 @@ export interface CloudAccount {
cloud_account_id: string;
config: AzureCloudAccountConfig | AWSCloudAccountConfig;
status: AccountStatus | IServiceStatus;
providerAccountId: string;
}
export interface AzureCloudAccountConfig {

View File

@@ -7,6 +7,13 @@ import {
GetIntegrationStatusProps,
} from 'types/api/integrations/types';
export function isOneClickIntegration(integrationId: string): boolean {
return (
integrationId === INTEGRATION_TYPES.AWS ||
integrationId === INTEGRATION_TYPES.AZURE
);
}
export const useGetIntegrationStatus = ({
integrationId,
}: GetIntegrationPayloadProps): UseQueryResult<
@@ -20,5 +27,5 @@ export const useGetIntegrationStatus = ({
enabled:
!!integrationId &&
integrationId !== '' &&
integrationId !== INTEGRATION_TYPES.AWS,
!isOneClickIntegration(integrationId),
});

View File

@@ -20,11 +20,11 @@ import {
CloudintegrationtypesCredentialsDTO,
CloudintegrationtypesPostableAccountDTO,
} from 'api/generated/services/sigNoz.schemas';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import {
ActiveViewEnum,
ModalStateEnum,
} from 'container/Integrations/CloudIntegration/AmazonWebServices/HeroSection/types';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
} from 'container/Integrations/HeroSection/types';
import useAxiosError from 'hooks/useAxiosError';
import { regions } from 'utils/regions';

View File

@@ -0,0 +1,142 @@
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { toast } from '@signozhq/ui';
import { Form } from 'antd';
import { FormInstance } from 'antd/lib';
import { useUpdateAccount } from 'api/generated/services/cloudintegration';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { CloudAccount } from 'container/Integrations/types';
import { isEqual } from 'lodash-es';
import logEvent from '../../../api/common/logEvent';
interface UseAccountSettingsModalProps {
onClose: () => void;
account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
}
interface UseAccountSettingsModal {
form: FormInstance;
isLoading: boolean;
resourceGroups: string[];
isSaveDisabled: boolean;
setResourceGroups: Dispatch<SetStateAction<string[]>>;
handleSubmit: () => Promise<void>;
handleClose: () => void;
}
export function useAccountSettingsModal({
onClose,
account,
setActiveAccount,
}: UseAccountSettingsModalProps): UseAccountSettingsModal {
const [form] = Form.useForm();
const { mutate: updateAccount, isLoading } = useUpdateAccount();
const accountConfig = useMemo(
() => ('deployment_region' in account.config ? account.config : null),
[account.config],
);
const [resourceGroups, setResourceGroups] = useState<string[]>(
accountConfig?.resource_groups || [],
);
useEffect(() => {
if (!accountConfig) {
return;
}
form.setFieldsValue({
region: accountConfig.deployment_region,
resourceGroups: accountConfig.resource_groups,
});
setResourceGroups(accountConfig.resource_groups);
}, [accountConfig, form]);
const handleSubmit = useCallback(async (): Promise<void> => {
try {
const values = await form.validateFields();
updateAccount(
{
pathParams: {
cloudProvider: INTEGRATION_TYPES.AZURE,
id: account?.id || '',
},
data: {
config: {
azure: {
resourceGroups: values.resourceGroups || [],
},
},
},
},
{
onSuccess: () => {
const nextConfig = {
deployment_region: accountConfig?.deployment_region || '',
resource_groups: values.resourceGroups || [],
};
setActiveAccount({
...account,
config: nextConfig,
});
onClose();
toast.success('Account settings updated successfully', {
position: 'bottom-right',
});
logEvent('Azure Integration: Account settings updated', {
cloudAccountId: account.cloud_account_id,
deploymentRegion: nextConfig.deployment_region,
resourceGroups: nextConfig.resource_groups,
});
},
onError: (error) => {
toast.error('Failed to update account settings', {
description: error?.message,
position: 'bottom-right',
});
},
},
);
} catch (error) {
console.error('Form submission failed:', error);
}
}, [form, updateAccount, account, setActiveAccount, onClose]);
const isSaveDisabled = useMemo(() => {
if (!accountConfig) {
return true;
}
const formResourceGroups = resourceGroups || [];
return isEqual(
[...formResourceGroups].sort(),
[...accountConfig.resource_groups].sort(),
);
}, [accountConfig, resourceGroups, form]);
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
return {
form,
isLoading,
resourceGroups,
isSaveDisabled,
setResourceGroups,
handleSubmit,
handleClose,
};
}

View File

@@ -0,0 +1,188 @@
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { useQueryClient } from 'react-query';
import { toast } from '@signozhq/ui';
import { Form, FormInstance } from 'antd';
import {
CreateAccountMutationResult,
GetConnectionCredentialsQueryResult,
invalidateListAccounts,
useCreateAccount,
useGetConnectionCredentials,
} from 'api/generated/services/cloudintegration';
import {
CloudintegrationtypesCredentialsDTO,
CloudintegrationtypesPostableAccountDTO,
} from 'api/generated/services/sigNoz.schemas';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { ModalStateEnum } from 'container/Integrations/HeroSection/types';
import useAxiosError from 'hooks/useAxiosError';
import logEvent from '../../../api/common/logEvent';
interface UseIntegrationModalProps {
onClose: () => void;
}
interface UseAzureIntegrationModal {
form: FormInstance;
modalState: ModalStateEnum;
isLoading: boolean;
accountId?: string;
connectionCommands: {
cliCommand: string;
cloudPowerShellCommand: string;
} | null;
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
handleSubmit: () => Promise<void>;
handleClose: () => void;
connectionParams?: CloudintegrationtypesCredentialsDTO;
isConnectionParamsLoading: boolean;
handleConnectionSuccess: (payload: {
cloudAccountId: string;
status?: unknown;
}) => void;
handleConnectionTimeout: (payload: { id?: string }) => void;
handleConnectionError: () => void;
}
export function useIntegrationModal({
onClose,
}: UseIntegrationModalProps): UseAzureIntegrationModal {
const queryClient = useQueryClient();
const [form] = Form.useForm();
const [modalState, setModalState] = useState<ModalStateEnum>(
ModalStateEnum.FORM,
);
const [isLoading, setIsLoading] = useState(false);
const [accountId, setAccountId] = useState<string | undefined>(undefined);
const [connectionCommands, setConnectionCommands] = useState<{
cliCommand: string;
cloudPowerShellCommand: string;
} | null>(null);
const handleClose = useCallback((): void => {
setModalState(ModalStateEnum.FORM);
setConnectionCommands(null);
onClose();
}, [onClose]);
const handleConnectionSuccess = useCallback(
(payload: { cloudAccountId: string; status?: unknown }): void => {
logEvent('Azure Integration: Account connected', {
cloudAccountId: payload.cloudAccountId,
status: payload.status,
});
toast.success('Azure account connected successfully', {
position: 'bottom-right',
});
void invalidateListAccounts(queryClient, {
cloudProvider: INTEGRATION_TYPES.AZURE,
});
handleClose();
},
[handleClose, queryClient],
);
const handleConnectionTimeout = useCallback(
(payload: { id?: string }): void => {
setModalState(ModalStateEnum.ERROR);
logEvent('Azure Integration: Account connection attempt timed out', {
id: payload.id,
});
},
[],
);
const handleConnectionError = useCallback((): void => {
setModalState(ModalStateEnum.ERROR);
}, []);
const { mutate: createAccount } = useCreateAccount();
const handleError = useAxiosError();
const { data: connectionParams, isLoading: isConnectionParamsLoading } =
useGetConnectionCredentials<GetConnectionCredentialsQueryResult>(
{
cloudProvider: INTEGRATION_TYPES.AZURE,
},
{
query: {
onError: handleError,
},
},
);
const handleSubmit = useCallback(async (): Promise<void> => {
try {
setIsLoading(true);
const values = await form.validateFields();
const payload: CloudintegrationtypesPostableAccountDTO = {
config: {
azure: {
deploymentRegion: values.region,
resourceGroups: values.resourceGroups || [],
},
},
credentials: {
ingestionUrl: connectionParams?.data?.ingestionUrl || values.ingestionUrl,
ingestionKey: connectionParams?.data?.ingestionKey || values.ingestionKey,
sigNozApiUrl: connectionParams?.data?.sigNozApiUrl || values.sigNozApiUrl,
sigNozApiKey: connectionParams?.data?.sigNozApiKey || values.sigNozApiKey,
},
};
createAccount(
{
pathParams: { cloudProvider: INTEGRATION_TYPES.AZURE },
data: payload,
},
{
onSuccess: (response: CreateAccountMutationResult) => {
const nextAccountId = response.data.id;
const artifact = response.data.connectionArtifact.azure;
logEvent('Azure Integration: Account connection commands generated', {
id: nextAccountId,
});
setConnectionCommands({
cliCommand: artifact?.cliCommand || '',
cloudPowerShellCommand: artifact?.cloudPowerShellCommand || '',
});
setModalState(ModalStateEnum.WAITING);
setAccountId(nextAccountId);
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
toast.error('Failed to create account connection', {
position: 'bottom-right',
});
},
},
);
} catch (error) {
console.error('Form submission failed:', error);
} finally {
setIsLoading(false);
}
}, [form, connectionParams, createAccount]);
return {
form,
modalState,
isLoading,
accountId,
connectionCommands,
setModalState,
handleSubmit,
handleClose,
connectionParams: connectionParams?.data as
| CloudintegrationtypesCredentialsDTO
| undefined,
isConnectionParamsLoading,
handleConnectionSuccess,
handleConnectionTimeout,
handleConnectionError,
};
}

View File

@@ -48,5 +48,23 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/onboarding", handler.New(
provider.authZ.ViewAccess(provider.infraMonitoringHandler.GetOnboarding),
handler.OpenAPIDef{
ID: "GetOnboarding",
Tags: []string{"inframonitoring"},
Summary: "Get Onboarding Status for Infra Monitoring",
Description: "Returns the per-tab readiness of the infra-monitoring section selected by the 'type' query parameter (hosts, processes, pods, nodes, deployments, daemonsets, statefulsets, jobs, namespaces, clusters, volumes). For each collector receiver or processor that contributes required metrics or attributes, lists what is present and what is missing, with a prebuilt user-facing message and a docs link per missing component. Default-enabled metrics are those expected as soon as the receiver is configured; optional metrics require 'enabled: true' in receiver config. 'ready' is true only when every missing list is empty.",
RequestQuery: new(inframonitoringtypes.PostableOnboarding),
Response: new(inframonitoringtypes.Onboarding),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -25,7 +25,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/user"
@@ -52,7 +51,6 @@ type provider struct {
flaggerHandler flagger.Handler
dashboardModule dashboard.Module
dashboardHandler dashboard.Handler
systemDashboardHandler systemdashboard.Handler
metricsExplorerHandler metricsexplorer.Handler
infraMonitoringHandler inframonitoring.Handler
gatewayHandler gateway.Handler
@@ -84,7 +82,6 @@ func NewFactory(
flaggerHandler flagger.Handler,
dashboardModule dashboard.Module,
dashboardHandler dashboard.Handler,
systemDashboardHandler systemdashboard.Handler,
metricsExplorerHandler metricsexplorer.Handler,
infraMonitoringHandler inframonitoring.Handler,
gatewayHandler gateway.Handler,
@@ -119,7 +116,6 @@ func NewFactory(
flaggerHandler,
dashboardModule,
dashboardHandler,
systemDashboardHandler,
metricsExplorerHandler,
infraMonitoringHandler,
gatewayHandler,
@@ -156,7 +152,6 @@ func newProvider(
flaggerHandler flagger.Handler,
dashboardModule dashboard.Module,
dashboardHandler dashboard.Handler,
systemDashboardHandler systemdashboard.Handler,
metricsExplorerHandler metricsexplorer.Handler,
infraMonitoringHandler inframonitoring.Handler,
gatewayHandler gateway.Handler,
@@ -191,7 +186,6 @@ func newProvider(
flaggerHandler: flaggerHandler,
dashboardModule: dashboardModule,
dashboardHandler: dashboardHandler,
systemDashboardHandler: systemDashboardHandler,
metricsExplorerHandler: metricsExplorerHandler,
infraMonitoringHandler: infraMonitoringHandler,
gatewayHandler: gatewayHandler,
@@ -260,10 +254,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addSystemDashboardRoutes(router); err != nil {
return err
}
if err := provider.addMetricsExplorerRoutes(router); err != nil {
return err
}

View File

@@ -1,66 +0,0 @@
package signozapiserver
import (
"net/http"
"github.com/gorilla/mux"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
)
func (provider *provider) addSystemDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/system/{source}", handler.New(provider.authZ.ViewAccess(provider.systemDashboardHandler.Get), handler.OpenAPIDef{
ID: "GetSystemDashboard",
Tags: []string{"systemdashboard"},
Summary: "Get system dashboard",
Description: "This endpoint returns the system dashboard for the callers org keyed by source (e.g. ai-o11y-overview).",
Request: nil,
RequestContentType: "",
Response: new(dashboardtypes.GettableDashboard),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/system/{source}", handler.New(provider.authZ.EditAccess(provider.systemDashboardHandler.Update), handler.OpenAPIDef{
ID: "UpdateSystemDashboard",
Tags: []string{"systemdashboard"},
Summary: "Update system dashboard",
Description: "This endpoint replaces the system dashboard for the callers org with the provided payload.",
Request: new(dashboardtypes.UpdatableDashboard),
RequestContentType: "application/json",
Response: new(dashboardtypes.GettableDashboard),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/system/{source}/reset", handler.New(provider.authZ.EditAccess(provider.systemDashboardHandler.Reset), handler.OpenAPIDef{
ID: "ResetSystemDashboard",
Tags: []string{"systemdashboard"},
Summary: "Reset system dashboard to defaults",
Description: "This resets edited/updated system dashboard to default system dashboard.",
Request: nil,
RequestContentType: "",
Response: new(dashboardtypes.GettableDashboard),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -22,7 +22,7 @@ func newConfig() factory.Config {
Agent: AgentConfig{
// we will maintain the latest version of cloud integration agent from here,
// till we automate it externally or figure out a way to validate it.
Version: "v0.0.9",
Version: "v0.0.10",
},
}
}

View File

@@ -38,7 +38,7 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
}
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard, "")
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard)
if err != nil {
return nil, err
}

View File

@@ -21,7 +21,7 @@ func NewStore(sqlstore sqlstore.SQLStore) dashboardtypes.Store {
func (store *store) Create(ctx context.Context, storabledashboard *dashboardtypes.StorableDashboard) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
BunDB().
NewInsert().
Model(storabledashboard).
Exec(ctx)
@@ -55,7 +55,6 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
Model(storableDashboard).
Where("id = ?", id).
Where("org_id = ?", orgID).
Where("source = ?", "").
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
@@ -64,23 +63,6 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
return storableDashboard, nil
}
func (store *store) GetBySource(ctx context.Context, orgID valuer.UUID, source string) (*dashboardtypes.StorableDashboard, error) {
storableDashboard := new(dashboardtypes.StorableDashboard)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(storableDashboard).
Where("org_id = ?", orgID).
Where("source = ?", source).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "system dashboard with source %s doesn't exist", source)
}
return storableDashboard, nil
}
func (store *store) GetPublic(ctx context.Context, dashboardID string) (*dashboardtypes.StorablePublicDashboard, error) {
storable := new(dashboardtypes.StorablePublicDashboard)
err := store.
@@ -142,7 +124,6 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardty
NewSelect().
Model(&storableDashboards).
Where("org_id = ?", orgID).
Where("source = ?", "").
Scan(ctx)
if err != nil {
return nil, err
@@ -169,16 +150,14 @@ func (store *store) ListPublic(ctx context.Context, orgID valuer.UUID) ([]*dashb
return storable, nil
}
// Update works for user dashboards (Source = "") and system dashboards (Source = "ai-o11y-overview").
func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableDashboard *dashboardtypes.StorableDashboard) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
BunDB().
NewUpdate().
Model(storableDashboard).
WherePK().
Where("org_id = ?", orgID).
Where("source = ?", storableDashboard.Source).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", storableDashboard.ID)
@@ -210,7 +189,6 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
Model(new(dashboardtypes.StorableDashboard)).
Where("id = ?", id).
Where("org_id = ?", orgID).
Where("source = ?", "").
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)

View File

@@ -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 {

View File

@@ -434,6 +434,57 @@ func (m *module) getMetricsExistenceAndEarliestTime(ctx context.Context, metricN
return missingMetrics, globalMinFirstReported, nil
}
// getAttributesExistence returns the subset of attrNames that are missing —
// i.e. have never 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) ([]string, error) {
if len(attrNames) == 0 {
return nil, 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()
present := make(map[string]bool, len(attrNames))
for rows.Next() {
var name string
var cnt uint64
if err := rows.Scan(&name, &cnt); err != nil {
return nil, err
}
if name != "" && cnt > 0 {
present[name] = true
}
}
if err := rows.Err(); err != nil {
return nil, err
}
var missing []string
for _, a := range attrNames {
if !present[a] {
missing = append(missing, a)
}
}
return missing, 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.

View File

@@ -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.
@@ -23,3 +25,50 @@ type podPhaseCounts struct {
Failed int
Unknown 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
}

View File

@@ -47,6 +47,80 @@ 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()
missingMetricsList, _, err := m.getMetricsExistenceAndEarliestTime(ctx, allMetrics)
if err != nil {
return nil, err
}
missingMetricsMap := make(map[string]bool, len(missingMetricsList))
for _, name := range missingMetricsList {
missingMetricsMap[name] = true
}
missingAttrsList, err := m.getAttributesExistence(ctx, allMetrics, allAttrs)
if err != nil {
return nil, err
}
missingAttrsMap := make(map[string]bool, len(missingAttrsList))
for _, name := range missingAttrsList {
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) {
if err := req.Validate(); err != nil {
return nil, err

View 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,
)
}

View File

@@ -0,0 +1,364 @@
package implinframonitoring
import "github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
// Component names — the 5 OTel collector receivers/processors that produce
// metrics and resource attributes consumed by infra-monitoring tabs. Bare
// strings on purpose (not wrapped enums) — the list is open-ended enough that
// an enum adds more friction than value.
const (
componentNameHostMetricsReceiver = "hostmetricsreceiver"
componentNameKubeletStatsReceiver = "kubeletstatsreceiver"
componentNameK8sClusterReceiver = "k8sclusterreceiver"
componentNameResourceDetectionProcessor = "resourcedetectionprocessor"
componentNameK8sAttributesProcessor = "k8sattributesprocessor"
)
// Documentation links — one per component. User-facing; emitted on missing-entries.
const (
docLinkHostMetricsReceiver = "https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#step-2-configure-the-collector"
docLinkKubeletStatsReceiver = "https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/#setting-up-kubelet-stats-monitoring"
docLinkK8sClusterReceiver = "https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/#setting-up-k8s-cluster-monitoring"
docLinkResourceDetectionProcessor = "https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty"
docLinkK8sAttributesProcessor = "https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/#2-enable-kubernetes-metadata"
)
var (
componentHostMetricsReceiver = inframonitoringtypes.AssociatedComponent{
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
Name: componentNameHostMetricsReceiver,
}
componentKubeletStatsReceiver = inframonitoringtypes.AssociatedComponent{
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
Name: componentNameKubeletStatsReceiver,
}
componentK8sClusterReceiver = inframonitoringtypes.AssociatedComponent{
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
Name: componentNameK8sClusterReceiver,
}
componentResourceDetectionProcessor = inframonitoringtypes.AssociatedComponent{
Type: inframonitoringtypes.OnboardingComponentTypeProcessor,
Name: componentNameResourceDetectionProcessor,
}
componentK8sAttributesProcessor = inframonitoringtypes.AssociatedComponent{
Type: inframonitoringtypes.OnboardingComponentTypeProcessor,
Name: componentNameK8sAttributesProcessor,
}
)
// 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)
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
Component: componentK8sAttributesProcessor,
RequiredAttrs: []string{"k8s.node.name"},
DocumentationLink: docLinkK8sAttributesProcessor,
},
},
}
var deploymentsSpec = onboardingSpec{
Buckets: []onboardingComponentBucket{
{
Component: componentKubeletStatsReceiver,
DefaultMetrics: []string{
"k8s.pod.cpu.usage",
"k8s.pod.memory.working_set",
},
OptionalMetrics: []string{
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
},
DocumentationLink: docLinkKubeletStatsReceiver,
},
{
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{
"k8s.container.restarts",
"k8s.deployment.desired",
"k8s.deployment.available",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
Component: componentK8sAttributesProcessor,
RequiredAttrs: []string{"k8s.deployment.name"},
DocumentationLink: docLinkK8sAttributesProcessor,
},
},
}
var daemonsetsSpec = onboardingSpec{
Buckets: []onboardingComponentBucket{
{
Component: componentKubeletStatsReceiver,
DefaultMetrics: []string{
"k8s.pod.cpu.usage",
"k8s.pod.memory.working_set",
},
OptionalMetrics: []string{
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
},
DocumentationLink: docLinkKubeletStatsReceiver,
},
{
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{
"k8s.container.restarts",
"k8s.daemonset.desired_scheduled_nodes",
"k8s.daemonset.current_scheduled_nodes",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
Component: componentK8sAttributesProcessor,
RequiredAttrs: []string{"k8s.daemonset.name"},
DocumentationLink: docLinkK8sAttributesProcessor,
},
},
}
var statefulsetsSpec = onboardingSpec{
Buckets: []onboardingComponentBucket{
{
Component: componentKubeletStatsReceiver,
DefaultMetrics: []string{
"k8s.pod.cpu.usage",
"k8s.pod.memory.working_set",
},
OptionalMetrics: []string{
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
},
DocumentationLink: docLinkKubeletStatsReceiver,
},
{
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{
"k8s.container.restarts",
"k8s.statefulset.desired_pods",
"k8s.statefulset.current_pods",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
Component: componentK8sAttributesProcessor,
RequiredAttrs: []string{"k8s.statefulset.name"},
DocumentationLink: docLinkK8sAttributesProcessor,
},
},
}
var jobsSpec = onboardingSpec{
Buckets: []onboardingComponentBucket{
{
Component: componentKubeletStatsReceiver,
DefaultMetrics: []string{
"k8s.pod.cpu.usage",
"k8s.pod.memory.working_set",
},
OptionalMetrics: []string{
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
},
DocumentationLink: docLinkKubeletStatsReceiver,
},
{
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{
"k8s.container.restarts",
"k8s.job.desired_successful_pods",
"k8s.job.active_pods",
"k8s.job.failed_pods",
"k8s.job.successful_pods",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
Component: componentK8sAttributesProcessor,
RequiredAttrs: []string{"k8s.job.name"},
DocumentationLink: docLinkK8sAttributesProcessor,
},
},
}
var namespacesSpec = onboardingSpec{
Buckets: []onboardingComponentBucket{
{
Component: componentKubeletStatsReceiver,
DefaultMetrics: []string{
"k8s.pod.cpu.usage",
"k8s.pod.memory.working_set",
},
DocumentationLink: docLinkKubeletStatsReceiver,
},
{
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{"k8s.pod.phase"},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
Component: componentK8sAttributesProcessor,
RequiredAttrs: []string{"k8s.namespace.name"},
DocumentationLink: docLinkK8sAttributesProcessor,
},
},
}
var clustersSpec = onboardingSpec{
Buckets: []onboardingComponentBucket{
{
Component: componentKubeletStatsReceiver,
DefaultMetrics: []string{
"k8s.node.cpu.usage",
"k8s.node.memory.working_set",
},
DocumentationLink: docLinkKubeletStatsReceiver,
},
{
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{
"k8s.node.allocatable_cpu",
"k8s.node.allocatable_memory", //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)
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
Component: componentResourceDetectionProcessor,
RequiredAttrs: []string{"k8s.cluster.name"},
DocumentationLink: docLinkResourceDetectionProcessor,
},
},
}
var volumesSpec = onboardingSpec{
Buckets: []onboardingComponentBucket{
{
Component: componentKubeletStatsReceiver,
DefaultMetrics: []string{
"k8s.volume.available",
"k8s.volume.capacity",
"k8s.volume.inodes",
"k8s.volume.inodes.free",
"k8s.volume.inodes.used",
},
DocumentationLink: docLinkKubeletStatsReceiver,
},
{
Component: componentK8sAttributesProcessor,
RequiredAttrs: []string{"k8s.persistentvolumeclaim.name"},
DocumentationLink: docLinkK8sAttributesProcessor,
},
},
}

View File

@@ -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, false)
requireMetricsEntry(t, "presentOptional", got.PresentOptional, tt.want.presentOptional, false)
requireAttrsEntry(t, "presentAttrs", got.PresentAttrs, tt.want.presentAttrs, false)
requireMissingMetrics(t, "missingDefault", got.MissingDefault, tt.want.missingDefault)
requireMissingMetrics(t, "missingOptional", got.MissingOptional, tt.want.missingOptional)
requireMissingAttrs(t, "missingAttrs", got.MissingAttrs, tt.want.missingAttrs)
})
}
}
func 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, _ bool) {
t.Helper()
if len(wantMetrics) == 0 {
require.Nil(t, got, name)
return
}
require.NotNil(t, got, name)
require.Equal(t, wantMetrics, got.Metrics, name)
require.Equal(t, testComponent, got.AssociatedComponent, name)
}
func requireAttrsEntry(t *testing.T, name string, got *inframonitoringtypes.AttributesComponentEntry, wantAttrs []string, _ bool) {
t.Helper()
if len(wantAttrs) == 0 {
require.Nil(t, got, name)
return
}
require.NotNil(t, got, name)
require.Equal(t, wantAttrs, got.Attributes, name)
require.Equal(t, testComponent, got.AssociatedComponent, name)
}
func requireMissingMetrics(t *testing.T, name string, got *inframonitoringtypes.MissingMetricsComponentEntry, wantMetrics []string) {
t.Helper()
if len(wantMetrics) == 0 {
require.Nil(t, got, name)
return
}
require.NotNil(t, got, name)
require.Equal(t, wantMetrics, got.Metrics, name)
require.Equal(t, testComponent, got.AssociatedComponent, name)
require.NotEmpty(t, got.Message, name)
require.Equal(t, testDocLink, got.DocumentationLink, name)
}
func requireMissingAttrs(t *testing.T, name string, got *inframonitoringtypes.MissingAttributesComponentEntry, wantAttrs []string) {
t.Helper()
if len(wantAttrs) == 0 {
require.Nil(t, got, name)
return
}
require.NotNil(t, got, name)
require.Equal(t, wantAttrs, got.Attributes, name)
require.Equal(t, testComponent, got.AssociatedComponent, name)
require.NotEmpty(t, got.Message, name)
require.Equal(t, testDocLink, got.DocumentationLink, name)
}

View File

@@ -11,9 +11,11 @@ import (
type Handler interface {
ListHosts(http.ResponseWriter, *http.Request)
ListPods(http.ResponseWriter, *http.Request)
GetOnboarding(http.ResponseWriter, *http.Request)
}
type Module interface {
ListHosts(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableHosts) (*inframonitoringtypes.Hosts, error)
ListPods(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostablePods) (*inframonitoringtypes.Pods, error)
GetOnboarding(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableOnboarding) (*inframonitoringtypes.Onboarding, error)
}

View File

@@ -6,20 +6,18 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type setter struct {
store types.OrganizationStore
alertmanager alertmanager.Alertmanager
quickfilter quickfilter.Module
systemDashboard systemdashboard.Module
store types.OrganizationStore
alertmanager alertmanager.Alertmanager
quickfilter quickfilter.Module
}
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module, systemDashboard systemdashboard.Module) organization.Setter {
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter, systemDashboard: systemDashboard}
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module) organization.Setter {
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter}
}
func (module *setter) Create(ctx context.Context, organization *types.Organization, createManagedRoles func(context.Context, valuer.UUID) error) error {
@@ -35,10 +33,6 @@ func (module *setter) Create(ctx context.Context, organization *types.Organizati
return err
}
if err := module.systemDashboard.SetDefaultConfig(ctx, organization.ID); err != nil {
return err
}
if err := createManagedRoles(ctx, organization.ID); err != nil {
return err
}

View File

@@ -1,102 +0,0 @@
package implsystemdashboard
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type handler struct {
module systemdashboard.Module
}
func NewHandler(module systemdashboard.Module) systemdashboard.Handler {
return &handler{module: module}
}
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), parseSource(r))
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, dashboard)
}
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
data := dashboardtypes.UpdatableDashboard{}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
render.Error(rw, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
return
}
if data == nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "data is required"))
return
}
dashboard, err := handler.module.Update(ctx, valuer.MustNewUUID(claims.OrgID), parseSource(r), &dashboardtypes.Dashboard{
Data: data,
UserAuditable: types.UserAuditable{UpdatedBy: claims.Email},
})
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, dashboard)
}
func (handler *handler) Reset(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := handler.module.Reset(ctx, valuer.MustNewUUID(claims.OrgID), parseSource(r))
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, dashboard)
}
// parseSource reads the {source} path segment.
func parseSource(r *http.Request) dashboardtypes.Source {
return dashboardtypes.Source(mux.Vars(r)["source"])
}

View File

@@ -1,138 +0,0 @@
package implsystemdashboard
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store dashboardtypes.Store
}
func NewModule(store dashboardtypes.Store) systemdashboard.Module {
return &module{store: store}
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) (*dashboardtypes.Dashboard, error) {
storable, err := module.store.GetBySource(ctx, orgID, string(source))
if err != nil {
return nil, err
}
return dashboardtypes.NewDashboardFromStorableDashboard(storable), nil
}
// Update applies the new payload as last-writer-wins. The Get and Update run inside one transaction so a
// concurrent Reset cannot interleave and leave the response with a stale id from before the reset.
func (module *module) Update(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source, dashboard *dashboardtypes.Dashboard) (*dashboardtypes.Dashboard, error) {
if dashboard == nil {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is required")
}
if dashboard.Data == nil {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard.Data is required")
}
var updated *dashboardtypes.Dashboard
err := module.store.RunInTx(ctx, func(ctx context.Context) error {
existing, err := module.store.GetBySource(ctx, orgID, string(source))
if err != nil {
return err
}
existing.Data = dashboard.Data
existing.UpdatedBy = dashboard.UpdatedBy
existing.UpdatedAt = time.Now()
if err := module.store.Update(ctx, orgID, existing); err != nil {
return err
}
updated = dashboardtypes.NewDashboardFromStorableDashboard(existing)
return nil
})
if err != nil {
return nil, err
}
return updated, nil
}
func (module *module) Reset(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) (*dashboardtypes.Dashboard, error) {
var reset *dashboardtypes.Dashboard
err := module.store.RunInTx(ctx, func(ctx context.Context) error {
defaultDashboard, err := dashboardtypes.NewDefaultSystemDashboard(orgID, source)
if err != nil {
return err
}
existing, err := module.store.GetBySource(ctx, orgID, string(source))
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
if existing == nil {
storable, err := dashboardtypes.NewStorableDashboardFromDashboard(defaultDashboard)
if err != nil {
return err
}
if err := module.store.Create(ctx, storable); err != nil {
return err
}
reset = defaultDashboard
return nil
}
existing.Data = defaultDashboard.Data
existing.UpdatedBy = "system"
existing.UpdatedAt = time.Now()
if err := module.store.Update(ctx, orgID, existing); err != nil {
return err
}
reset = dashboardtypes.NewDashboardFromStorableDashboard(existing)
return nil
})
if err != nil {
return nil, err
}
return reset, nil
}
func (module *module) SetDefaultConfig(ctx context.Context, orgID valuer.UUID) error {
for _, source := range dashboardtypes.SystemSources {
if err := module.setDefaultForSource(ctx, orgID, source); err != nil {
return err
}
}
return nil
}
func (module *module) setDefaultForSource(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) error {
existing, err := module.store.GetBySource(ctx, orgID, string(source))
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
if existing != nil {
return nil
}
dashboard, err := dashboardtypes.NewDefaultSystemDashboard(orgID, source)
if err != nil {
return err
}
storable, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
if err != nil {
return err
}
return module.store.Create(ctx, storable)
}

View File

@@ -1,25 +0,0 @@
package systemdashboard
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
Get(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) (*dashboardtypes.Dashboard, error)
Update(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source, dashboard *dashboardtypes.Dashboard) (*dashboardtypes.Dashboard, error)
Reset(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) (*dashboardtypes.Dashboard, error)
SetDefaultConfig(ctx context.Context, orgID valuer.UUID) error
}
// Handler defines the HTTP handler interface for system dashboard endpoints.
// /api/v1/system/{source} — Get / Update
// /api/v1/system/{source}/reset — Reset.
type Handler interface {
Get(http.ResponseWriter, *http.Request)
Update(http.ResponseWriter, *http.Request)
Reset(http.ResponseWriter, *http.Request)
}

View File

@@ -40,8 +40,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/spanmapper/implspanmapper"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard/implsystemdashboard"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
@@ -57,7 +55,6 @@ type Handlers struct {
SavedView savedview.Handler
Apdex apdex.Handler
Dashboard dashboard.Handler
SystemDashboard systemdashboard.Handler
QuickFilter quickfilter.Handler
TraceFunnel tracefunnel.Handler
RawDataExport rawdataexport.Handler
@@ -102,7 +99,6 @@ func NewHandlers(
SavedView: implsavedview.NewHandler(modules.SavedView),
Apdex: implapdex.NewHandler(modules.Apdex),
Dashboard: impldashboard.NewHandler(modules.Dashboard, providerSettings, authz),
SystemDashboard: implsystemdashboard.NewHandler(modules.SystemDashboard),
QuickFilter: implquickfilter.NewHandler(modules.QuickFilter),
TraceFunnel: impltracefunnel.NewHandler(modules.TraceFunnel),
RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport),

View File

@@ -17,7 +17,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -41,8 +40,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/session/implsession"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard/implsystemdashboard"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
@@ -69,7 +66,6 @@ type Modules struct {
SavedView savedview.Module
Apdex apdex.Module
Dashboard dashboard.Module
SystemDashboard systemdashboard.Module
QuickFilter quickfilter.Module
TraceFunnel tracefunnel.Module
RawDataExport rawdataexport.Module
@@ -110,8 +106,7 @@ func NewModules(
fl flagger.Flagger,
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
systemDashboard := implsystemdashboard.NewModule(impldashboard.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter, systemDashboard)
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
userSetter := impluser.NewSetter(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User, userRoleStore, userGetter)
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
@@ -122,7 +117,6 @@ func NewModules(
SavedView: implsavedview.NewModule(sqlstore),
Apdex: implapdex.NewModule(sqlstore),
Dashboard: dashboard,
SystemDashboard: systemDashboard,
UserSetter: userSetter,
UserGetter: userGetter,
QuickFilter: quickfilter,

View File

@@ -30,7 +30,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/user"
@@ -64,7 +63,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ flagger.Handler }{},
struct{ dashboard.Module }{},
struct{ dashboard.Handler }{},
struct{ systemdashboard.Handler }{},
struct{ metricsexplorer.Handler }{},
struct{ inframonitoring.Handler }{},
struct{ gateway.Handler }{},

View File

@@ -195,7 +195,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewServiceAccountAuthzactory(sqlstore),
sqlmigration.NewDropUserDeletedAtFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateAWSAllRegionsFactory(sqlstore),
sqlmigration.NewAddSystemDashboardFactory(sqlstore, sqlschema),
)
}
@@ -270,7 +269,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.FlaggerHandler,
modules.Dashboard,
handlers.Dashboard,
handlers.SystemDashboard,
handlers.MetricsExplorer,
handlers.InfraMonitoring,
handlers.GatewayHandler,

View File

@@ -1,113 +0,0 @@
package sqlmigration
import (
"context"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type addSystemDashboard struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddSystemDashboardFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_system_dashboard"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addSystemDashboard{sqlstore: sqlstore, sqlschema: sqlschema}, nil
})
}
func (migration *addSystemDashboard) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addSystemDashboard) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, "dashboard")
if err != nil {
return err
}
column := &sqlschema.Column{
Name: sqlschema.ColumnName("source"),
DataType: sqlschema.DataTypeText,
Nullable: false,
}
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, "")
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
var orgIDs []string
if err := tx.NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs); err != nil {
return err
}
for _, rawOrgID := range orgIDs {
orgID, err := valuer.NewUUID(rawOrgID)
if err != nil {
return err
}
for _, source := range dashboardtypes.SystemSources {
count, err := tx.NewSelect().
Model((*dashboardtypes.StorableDashboard)(nil)).
Where("org_id = ?", orgID).
Where("source = ?", string(source)).
Count(ctx)
if err != nil {
return err
}
if count > 0 {
continue
}
dashboard, err := dashboardtypes.NewDefaultSystemDashboard(orgID, source)
if err != nil {
return err
}
storable, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
if err != nil {
return err
}
if _, err := tx.NewInsert().Model(storable).Exec(ctx); err != nil {
return err
}
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *addSystemDashboard) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -1,107 +0,0 @@
{
"title": "AI Observability Overview",
"description": "AI / LLM observability overview — cost, tokens, latency, errors, RED for tool calls, and time to first token. Scoped by model, environment and service (apply via the variable bar).",
"tags": ["ai", "llm", "genai", "overview"],
"version": "v5",
"variables": {
"model": {
"id": "a1000000-0000-0000-0000-000000000001",
"name": "model",
"key": "model",
"description": "LLM model",
"type": "QUERY",
"sort": "ASC",
"multiSelect": true,
"showALLOption": true,
"allSelected": true,
"queryValue": "SELECT DISTINCT attributes_string['gen_ai.request.model'] AS model FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(attributes_string, 'gen_ai.request.model') AND timestamp >= now() - INTERVAL 1 DAY",
"customValue": "",
"textboxValue": "",
"selectedValue": [],
"order": 0,
"modificationUUID": "a1000000-0000-0000-0000-000000000011"
},
"environment": {
"id": "a1000000-0000-0000-0000-000000000002",
"name": "environment",
"key": "environment",
"description": "Deployment environment",
"type": "QUERY",
"sort": "ASC",
"multiSelect": true,
"showALLOption": true,
"allSelected": true,
"queryValue": "SELECT DISTINCT resources_string['deployment.environment'] AS environment FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(resources_string, 'deployment.environment') AND timestamp >= now() - INTERVAL 1 DAY",
"customValue": "",
"textboxValue": "",
"selectedValue": [],
"order": 1,
"modificationUUID": "a1000000-0000-0000-0000-000000000012"
},
"service_name": {
"id": "a1000000-0000-0000-0000-000000000003",
"name": "service_name",
"key": "service_name",
"description": "Service name",
"type": "QUERY",
"sort": "ASC",
"multiSelect": true,
"showALLOption": true,
"allSelected": true,
"queryValue": "SELECT DISTINCT resources_string['service.name'] AS service_name FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(resources_string, 'service.name') AND timestamp >= now() - INTERVAL 1 DAY",
"customValue": "",
"textboxValue": "",
"selectedValue": [],
"order": 2,
"modificationUUID": "a1000000-0000-0000-0000-000000000013"
}
},
"layout": [
{"i": "11111111-1111-1111-1111-111111111111", "x": 0, "y": 0, "w": 3, "h": 3, "moved": false, "static": false},
{"i": "22222222-2222-2222-2222-222222222222", "x": 3, "y": 0, "w": 3, "h": 3, "moved": false, "static": false},
{"i": "33333333-3333-3333-3333-333333333333", "x": 6, "y": 0, "w": 2, "h": 3, "moved": false, "static": false},
{"i": "44444444-4444-4444-4444-444444444444", "x": 8, "y": 0, "w": 2, "h": 3, "moved": false, "static": false},
{"i": "55555555-5555-5555-5555-555555555555", "x": 10, "y": 0, "w": 2, "h": 3, "moved": false, "static": false},
{"i": "66666666-6666-6666-6666-666666666666", "x": 0, "y": 3, "w": 6, "h": 4, "moved": false, "static": false},
{"i": "77777777-7777-7777-7777-777777777777", "x": 6, "y": 3, "w": 6, "h": 4, "moved": false, "static": false},
{"i": "88888888-8888-8888-8888-888888888888", "x": 0, "y": 7, "w": 6, "h": 4, "moved": false, "static": false},
{"i": "99999999-9999-9999-9999-999999999999", "x": 6, "y": 7, "w": 6, "h": 4, "moved": false, "static": false},
{"i": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "x": 0, "y": 11, "w": 4, "h": 4, "moved": false, "static": false},
{"i": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "x": 4, "y": 11, "w": 4, "h": 4, "moved": false, "static": false},
{"i": "cccccccc-cccc-cccc-cccc-cccccccccccc", "x": 8, "y": 11, "w": 4, "h": 4, "moved": false, "static": false},
{"i": "dddddddd-dddd-dddd-dddd-dddddddddddd", "x": 0, "y": 15, "w": 4, "h": 4, "moved": false, "static": false},
{"i": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "x": 4, "y": 15, "w": 4, "h": 4, "moved": false, "static": false},
{"i": "ffffffff-ffff-ffff-ffff-ffffffffffff", "x": 8, "y": 15, "w": 4, "h": 4, "moved": false, "static": false}
],
"widgets": [
{"id": "11111111-1111-1111-1111-111111111111", "title": "Total cost", "description": "Total LLM cost across all calls. Requires gen_ai.usage.cost attribute.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "none", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q1111111-1111-1111-1111-111111111111", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.cost)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "sum"}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "22222222-2222-2222-2222-222222222222", "title": "Total tokens", "description": "Sum of input + output tokens.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "short", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q2222222-2222-2222-2222-222222222222", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.input_tokens) + sum(gen_ai.usage.output_tokens)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "sum"}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "33333333-3333-3333-3333-333333333333", "title": "Avg latency (p95)", "description": "p95 latency of LLM spans.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ms", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q3333333-3333-3333-3333-333333333333", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p95(duration_nano) / 1000000"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "avg"}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "44444444-4444-4444-4444-444444444444", "title": "Error rate", "description": "Error rate as a percentage of total LLM calls.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "percent", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q4444444-4444-4444-4444-444444444444", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "countIf(has_error = true) * 100 / count()"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "avg"}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "55555555-5555-5555-5555-555555555555", "title": "TTFT (p95)", "description": "p95 time to first token.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ms", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q5555555-5555-5555-5555-555555555555", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p95(gen_ai.server.ttft)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "avg"}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "66666666-6666-6666-6666-666666666666", "title": "Cost over time", "description": "Cost by model over time.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "none", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q6666666-6666-6666-6666-666666666666", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.cost)"}], "filter": {"expression": ""}, "groupBy": [{"key": "gen_ai.request.model", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.request.model}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "77777777-7777-7777-7777-777777777777", "title": "Token usage over time", "description": "Input vs output tokens over time.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": true, "fillSpans": false, "yAxisUnit": "short", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q7777777-7777-7777-7777-777777777777", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.input_tokens)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "Input", "disabled": false, "having": {"expression": ""}, "limit": null}, {"queryName": "B", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.output_tokens)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "B", "orderBy": [], "legend": "Output", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "88888888-8888-8888-8888-888888888888", "title": "LLM call latency percentiles", "description": "p50, p90, p95, p99 latency by model.", "panelTypes": "table", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ms", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q8888888-8888-8888-8888-888888888888", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p50(duration_nano) / 1000000"}, {"expression": "p90(duration_nano) / 1000000"}, {"expression": "p95(duration_nano) / 1000000"}, {"expression": "p99(duration_nano) / 1000000"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [{"key": "gen_ai.request.model", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.request.model}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "99999999-9999-9999-9999-999999999999", "title": "LLM call latency over time", "description": "p95 latency trend by model.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ms", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q9999999-9999-9999-9999-999999999999", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p95(duration_nano) / 1000000"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [{"key": "gen_ai.request.model", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.request.model}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "title": "Error count", "description": "Errors grouped by error type.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": true, "fillSpans": false, "yAxisUnit": "short", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "count()"}], "filter": {"expression": "has_error = true AND gen_ai.system != ''"}, "groupBy": [{"key": "gen_ai.error.type", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.error.type}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "title": "Time to first token", "description": "p95 TTFT by model.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ms", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p95(gen_ai.server.ttft)"}], "filter": {"expression": ""}, "groupBy": [{"key": "gen_ai.request.model", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.request.model}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "title": "Top 10 span names", "description": "Top span names by count across GenAI spans.", "panelTypes": "table", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "none", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qccccccc-cccc-cccc-cccc-cccccccccccc", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "count()"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [{"key": "name", "dataType": "string", "type": "tag", "isColumn": true, "isJSON": false}], "expression": "A", "orderBy": [{"columnName": "count()", "order": "desc"}], "legend": "{{name}}", "disabled": false, "having": {"expression": ""}, "limit": 10}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "dddddddd-dddd-dddd-dddd-dddddddddddd", "title": "Tool call rate", "description": "Tool call rate per second.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "reqps", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qddddddd-dddd-dddd-dddd-dddddddddddd", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "rate()"}], "filter": {"expression": "name = 'execute_tool'"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "req/s", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "title": "Tool error rate", "description": "Percentage of tool calls that errored.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "percent", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "countIf(has_error = true) * 100 / count()"}], "filter": {"expression": "name = 'execute_tool'"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "error %", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
{"id": "ffffffff-ffff-ffff-ffff-ffffffffffff", "title": "Tool duration (p50)", "description": "Median tool call duration.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ms", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qfffffff-ffff-ffff-ffff-ffffffffffff", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p50(duration_nano) / 1000000"}], "filter": {"expression": "name = 'execute_tool'"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "p50", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}}
]
}

View File

@@ -37,7 +37,6 @@ type StorableDashboard struct {
Data StorableDashboardData `bun:"data,type:text,notnull"`
Locked bool `bun:"locked,notnull,default:false"`
OrgID valuer.UUID `bun:"org_id,notnull"`
Source string `bun:"source,type:text,notnull"`
}
type Dashboard struct {
@@ -48,7 +47,6 @@ type Dashboard struct {
Data StorableDashboardData `json:"data"`
Locked bool `json:"locked"`
OrgID valuer.UUID `json:"org_id"`
Source string `json:"source"`
}
type LockUnlockDashboard struct {
@@ -88,11 +86,10 @@ func NewStorableDashboardFromDashboard(dashboard *Dashboard) (*StorableDashboard
OrgID: dashboard.OrgID,
Data: dashboard.Data,
Locked: dashboard.Locked,
Source: dashboard.Source,
}, nil
}
func NewDashboard(orgID valuer.UUID, createdBy string, data StorableDashboardData, source Source) (*Dashboard, error) {
func NewDashboard(orgID valuer.UUID, createdBy string, storableDashboardData StorableDashboardData) (*Dashboard, error) {
currentTime := time.Now()
return &Dashboard{
@@ -106,8 +103,8 @@ func NewDashboard(orgID valuer.UUID, createdBy string, data StorableDashboardDat
UpdatedBy: createdBy,
},
OrgID: orgID,
Data: data,
Source: string(source),
Data: storableDashboardData,
Locked: false,
}, nil
}
@@ -125,7 +122,6 @@ func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) *Da
OrgID: storableDashboard.OrgID,
Data: storableDashboard.Data,
Locked: storableDashboard.Locked,
Source: storableDashboard.Source,
}
}
@@ -158,7 +154,6 @@ func NewGettableDashboardFromDashboard(dashboard *Dashboard) (*GettableDashboard
OrgID: dashboard.OrgID,
Data: dashboard.Data,
Locked: dashboard.Locked,
Source: dashboard.Source,
}, nil
}

View File

@@ -66,7 +66,7 @@ func TestCanUpdate_MultipleDeletions_ByDiff(t *testing.T) {
initial := StorableDashboardData{
"widgets": makeTestWidgets("a", "b", "c"),
}
d, err := NewDashboard(orgID, "tester", initial, "")
d, err := NewDashboard(orgID, "tester", initial)
assert.NoError(t, err)
updated := StorableDashboardData{

View File

@@ -1,40 +0,0 @@
package dashboardtypes
import (
_ "embed"
"encoding/json"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Source string
const (
SourceAIO11yOverview Source = "ai-o11y-overview"
)
var SystemSources = []Source{
SourceAIO11yOverview,
}
//go:embed ai_o11y_overview.json
var aiO11yOverviewJSON []byte
func NewDefaultSystemDashboard(orgID valuer.UUID, source Source) (*Dashboard, error) {
switch source {
case SourceAIO11yOverview:
return newDefaultAIO11yOverview(orgID)
default:
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "no defaults registered for system dashboard source %s", source)
}
}
func newDefaultAIO11yOverview(orgID valuer.UUID) (*Dashboard, error) {
data := StorableDashboardData{}
if err := json.Unmarshal(aiO11yOverviewJSON, &data); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal embedded ai-o11y-overview default")
}
return NewDashboard(orgID, "system", data, SourceAIO11yOverview)
}

View File

@@ -13,8 +13,6 @@ type Store interface {
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableDashboard, error)
GetBySource(context.Context, valuer.UUID, string) (*StorableDashboard, error)
GetPublic(context.Context, string) (*StorablePublicDashboard, error)
GetDashboardByOrgsAndPublicID(context.Context, []string, string) (*StorableDashboard, error)

View 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"`
}

View 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,
}
}

View 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])
}
}