Compare commits

...

51 Commits

Author SHA1 Message Date
ahmadshaheer
fd765f8646 chore: remove console.log 2025-02-06 20:56:05 +04:30
ahmadshaheer
5bb3563713 fix: handle all null autocomplete aggregateValues 2025-02-06 20:48:16 +04:30
Srikanth Chekuri
6d8d2e6b11 chore: use any of [cpu.usage, cpu.utilization] for cpu (#7017) 2025-02-06 11:56:58 +00:00
Srikanth Chekuri
a8e8f31b00 chore: parse string values for __value filter (#7035) 2025-02-06 11:47:29 +00:00
Shaheer Kochai
c3164912e6 AWS Integration changes (#7025)
* fix: update AWS accounts API response to return accounts list

* feat: display skeleton UI for account actions and refactored rendering logic

* chore: update AWS service naming from "AWS Web Services" to "Amazon Web Services"

* feat: aws integration success modal changes

* feat: auto-select first service when no service is active

* feat: display 'enable service' if service hasn't been configured and 'Configure (x/2)' if configured

* fix: display no data yet if status is not available

* feat: properly handle remove integration account flow

* fix: rename accountId param to cloudAccountId

* fix: update the aws service list and details api parameter from account_id to cloud_account_id

* fix: fix the issue of stale service config modal enabled/disabled state

* chore: improve the UI of configure button

* feat: add connection parameters support for AWS cloud integration

* feat: add optional link support for cloud service dashboards

* fix: get the correct supported signals count + a minor refactoring

* fix: remove cloudAccountId on success of account remove

* chore: update the remove integration copy

* refactor: add react query key for AWS connection parameters

* fix: correct typo in integration loading state variable name

* refactor: move skeleton inline styles to style file and do overall refactoring

* chore: address the requested changes
2025-02-06 15:13:19 +04:30
Shaheer Kochai
b215c6a0ce feat: aws integration feature flag changes (#7033)
* feat: aws integration feature flag changes

* fix: fix the failing build
2025-02-06 09:26:10 +00:00
Ekansh Gupta
94c2398a08 feat: bug fix for segregating entrypoint spans and root spans in span filtering (#7046) 2025-02-06 13:26:18 +05:30
Raj Kamal Singh
acd9b97ee3 Feat: aws integrations: service connection status (#7032)
* feat: add ability to get latest metric received ts by labelValues filter

* feat: svc metrics connection status check

* feat: aws integration svc logs connection check

* chore: fix broken test

* chore: address PR review comments

* chore: address PR feedback

* chore: fix broken test expectation

* fix: use resource filter for logs connection status
2025-02-06 13:08:47 +05:30
Srikanth Chekuri
c5219ac157 chore: add related values (#6619) 2025-02-06 05:09:01 +00:00
Vikrant Gupta
2b32ce190f task(licenses): update the license events and the state names (#7034)
* Revert "Revert "chore(licenses): update the license events and the state name…"

This reverts commit 66adc7fbf9.

* chore(license): fix comment
2025-02-05 18:25:36 +00:00
Amlan Kumar Nandy
c7c7b25651 feat: metrics explorer base setup (#7024) 2025-02-05 12:57:12 +00:00
Amlan Kumar Nandy
f548afe284 chore: share filters across logs and traces views in entity details (#7014) 2025-02-05 18:18:35 +05:30
primus-bot[bot]
586f5255f0 chore(release): bump to v0.71.0 (#7031)
#### Summary
 - Release SigNoz v0.71.0
 - Bump SigNoz OTel Collector to v0.111.26

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2025-02-05 15:13:58 +05:30
SagarRajput-7
62064f136d feat: corrected the color map for error % and added throughput unit (#7030) 2025-02-05 14:56:31 +05:30
Vikrant Gupta
66adc7fbf9 Revert "chore(licenses): update the license events and the state names (#7021)" (#7029)
This reverts commit e414215786.
2025-02-05 12:38:56 +05:30
Yunus M
f6b2d5a519 feat: change grid to flex - flower metrics section (#7027) 2025-02-05 11:48:44 +05:30
Vibhu Pandey
035999250e fix(add_pats): fix name of column (#7026) 2025-02-05 11:02:17 +05:30
Shaheer Kochai
aecbb71ce4 feat: truncate very long lines in traces explorer list view (#6987)
* feat: truncate very long lines in traces explorer list view

* fix: update snapshot tests with new line-clamped text class to fix the failing test
2025-02-04 13:29:23 +00:00
Ekansh Gupta
01b6e22bbd feat: bug fix for the issue in span filtering feature. No spans returned in traces tab (#7023)
* feat: bug fix for the issue in span filtering feature. No spans returned in traces tab

* feat: bug fix for the issue in span filtering feature. No spans returned in traces tab
2025-02-04 18:36:07 +05:30
Vibhu Pandey
dc15ee8176 feat(sqlmigration): consolidate all sqlmigrations into one package (#7018)
* feat(sqlmigration): add sqlmigrations

* feat(sqlmigration): test sqlmigrations

* feat(sqlmigration): add remaining factories

* feat(sqlmigration): consolidate into single package

* fix(telemetrystore): remove existing env variables

* fix(telemetrystore): fix DSN
2025-02-04 09:23:36 +00:00
Vikrant Gupta
e414215786 chore(licenses): update the license events and the state names (#7021) 2025-02-04 14:11:13 +05:30
Amlan Kumar Nandy
5fe04078e5 chore: update logs in infra monitoring for analytics (#6994) 2025-02-04 06:01:37 +00:00
SagarRajput-7
cf95b15ba1 feat: celery - misc feedback fixes (#7019)
* feat: celery - misc feedback fixes

* feat: enabled better sharing and auto-refresh

* feat: made task name filter more visible
2025-02-04 09:41:27 +05:30
Raj Kamal Singh
3b550c485d Feat: cloud integrations: agent check-in api (#7004)
* chore: cloudintegrations: rename rds def

* feat: cloudintegrations: shape of agent check in response for v0 release

* chore: cloudintegrations: add validation for response expected after agent check in

* chore: cloudintegrations: accumulate teletry collection strategies for enabled services

* chore: cloudintegrations: use map struct to parse from map to struct with json tags

* chore: cloudintegrations: telemetry collection strategy for services

* chore: cloudintegrations: wrap up test for agent check in resp

* chore: some cleanup

* chore: some cleanup

* chore: some minor renaming

* chore: address review comment

* chore: some cleanup

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-02-03 20:52:15 +05:30
Amlan Kumar Nandy
784dccf298 feat: fix incorrect suggestions while moving between explores (#7008) 2025-02-03 14:31:01 +05:30
Shaheer Kochai
aa26dc77af fix(styles): fix the gap between text and button in logs quick filters (#7006) 2025-02-03 04:54:21 +00:00
Raj Kamal Singh
e33a0fdd47 Feat/cloud integrations connection params api key (#6997)
* feat: get started on PAT provisioning for AWS integration

* chore: include cloud integration PAT in connection params

* chore: some cleanup

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-02-01 20:12:56 +05:30
Srikanth Chekuri
c8032f771e chore: add k8s metrics receiving status (#6977) 2025-01-31 15:07:04 +00:00
primus-bot[bot]
536656281d chore(release): bump to v0.70.1 (#7000)
#### Summary
 - Release SigNoz v0.70.1
 - Bump SigNoz OTel Collector to v0.111.25

 Created by [Primus-Bot](https://github.com/apps/primus-bot)

Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
Co-authored-by: Prashant Shahi <prashant@signoz.io>
2025-01-31 18:50:32 +05:30
Prashant Shahi
c6fda99b9b fix(query-service): skip preloading non-json files under dashboards (#6979)
### Summary

- query-service: skip preloading non-json files under dashboards

Resolves the error log:

```
{"level":"INFO","timestamp":"2025-01-29T17:37:18.635Z","caller":"dashboards/provision.go:26","msg":"Provisioning dashboard: ","filename":".gitkeep"}
{"level":"ERROR","timestamp":"2025-01-29T17:37:18.635Z","caller":"dashboards/provision.go:38","msg":"Creating Dashboards: Error in unmarshalling json from file","filename":".gitkeep","error":"unexpected end of JSON input","stacktrace":"go.signoz.io/signoz/pkg/query-service/app/dashboards.readCurrentDir\n\t/home/sa_100696978840572610735/signoz/pkg/query-service/app/dashboards/provision.go:38\ngo.signoz.io/signoz/pkg/query-service/app/dashboards.LoadDashboardFiles\n\t/home/sa_100696978840572610735/signoz/pkg/query-service/app/dashboards/provision.go:79\ngo.signoz.io/signoz/pkg/query-service/app.NewAPIHandler\n\t/home/sa_100696978840572610735/signoz/pkg/query-service/app/http_handler.go:271\ngo.signoz.io/signoz/ee/query-service/app/api.NewAPIHandler\n\t/home/sa_100696978840572610735/signoz/ee/query-service/app/api/api.go:56\ngo.signoz.io/signoz/ee/query-service/app.NewServer\n\t/home/sa_100696978840572610735/signoz/ee/query-service/app/server.go:287\nmain.main\n\t/home/sa_100696978840572610735/signoz/ee/query-service/main.go:191\nruntime.main\n\t/home/sa_100696978840572610735/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.7.linux-amd64/src/runtime/proc.go:271"}
```

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-01-31 13:09:47 +00:00
Vikrant Gupta
bbf64a7b52 fix(trace-detail): trace details improvements (#6998)
* fix(trace-detail): query service and ux improvements

* fix(trace-detail): added query service events

* fix(trace-detail): address review comments
2025-01-31 15:04:21 +05:30
Amlan Kumar Nandy
5783f1555f chore: add functionality to open entity details from nested table items in groupby mode (#6981) 2025-01-31 07:54:14 +00:00
Raj Kamal Singh
93a8f97355 Feat: cloud integrations: generate connection params (#6982)
* feat: include GatewayUrl in APIHandler options

* feat: logic for getting or creating an ingestion key when available

* feat: helper for generating ingestion url

* feat: also include deduced SigNoz API URL in connection params response

* chore: some cleanup

* chore: some more cleanup

* chore: address a PR comment

* chore: some cleanup

* chore: pass down context
2025-01-31 12:40:04 +05:30
Shaheer Kochai
a84e462a65 Fix/check the flows of toggling logs timestamp and body columns (#6992)
* fix: add default timestamp and body columns to live logs

* refactor: use convertKeysToColumnFields instead of re-modifying the default columns

* fix: remove the local storage selectColumns when clear view is clicked

* fix: improve selectColumns migration handling
2025-01-31 11:25:47 +05:30
Shaheer Kochai
d910d99689 fix: raw log data not rendering in log context (#6991)
* feat: add default selected fields for log context view

* fix: add null check to to traceData

* refactor: address review comment
2025-01-30 18:16:55 +00:00
Yunus M
e542e96031 feat: aws integration (#6954)
* feat: aws Integration skeleton UI (#6758)

* feat: add AWS integration in the integrations list and redirect to the new Cloud Integration page

* feat: cloud integration details page header (i.e. breadcrumb and get help button) UI

* feat: hero section UI

* refactor: extract Header and HeroSection components from CloudIntegrationPage

* feat: services tab bar and sidebar UI

* feat: cloud integration details services UI

* refactor: group and extract cloud integration components to files

* fix: set default active service to the first service in the list if no service is specified

* feat: add NEW flag for AWS integration in the integrations list page

* chore: overall improvements

* chore: move cloud integration pages to /container

* fix: hero section background

* feat: aws Integration: Account setup basic UI and functionality (#6806)

* feat: implement basic cloud account management UI in HeroSection

* feat: aws Integration: Integrate now modal (#6807)

* feat: implement basic cloud account management UI in HeroSection

* feat: start working on integrate now modal UI

* feat: integrate now modal UI

* feat: integrate now modal states and json server API integration

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* feat: display error state if last_heartbeat_ts_ms is null even after 5 minutes

* chore: update import path for regions data in useRegionSelection hook

* chore: move hero section components inside the HeroSection/components

* feat: create a reusable modal component

* refactor: make the cloud account setup modal readable / DRYer

* feat: aws Integration: Account settings modal (#6808)

* feat: implement basic cloud account management UI in HeroSection

* feat: start working on integrate now modal UI

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* feat: integrate now modal UI

* feat: integrate now modal states and json server API integration

* feat: account settings

* feat: service status UI

* refactor: make account settings modal more readable and overall improvements

* feat: Get data from json server api data in service sections (#6809)

* feat: implement basic cloud account management UI in HeroSection

* feat: start working on integrate now modal UI

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* refactor: make the cloud account setup modal readable / DRYer

* feat: integrate now modal states and json server API integration

* refactor: make account settings modal more readable and overall improvements

* feat: integrate now modal states and json server API integration

* feat: display error state if last_heartbeat_ts_ms is null even after 5 minutes

* feat: get the services list and details from json server API response

* feat: update account actions to set accountId in URL query on initial account load

* feat: configure service modal (#6814)

* feat: implement basic cloud account management UI in HeroSection

* feat: start working on integrate now modal UI

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* refactor: make the cloud account setup modal readable / DRYer

* feat: integrate now modal states and json server API integration

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* feat: integrate now modal states and json server API integration

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* feat: display error state if last_heartbeat_ts_ms is null even after 5 minutes

* feat: account settings

* feat: service status UI

* feat: get the services list and details from json server API response

* feat: update account actions to set accountId in URL query on initial account load

* feat: configure service modal UI

* feat: configure service modal functionality and API changes

* feat: replace loading indicators with Spinner component in ServiceDetails and ServicesList

* fix: make the configure service modal work

* feat: light mode support and overall improvements to AWS integration page (#6817)

* refactor: make the cloud account setup modal readable / DRYer

* feat: integrate now modal states and json server API integration

* refactor: make account settings modal more readable and overall improvements

* fix: integrate now modal button improvements

* feat: aws integrations light mode

* refactor: overall improvements

* refactor: define react query keys in constant

* feat: services filter

* feat: render service overview as markdown

* feat: integrate AWS integration page API (#6851)

* feat: replace json-server APIs with actual APIs

* fix: add null checks and fix the issues

* chore: remove the console.log

* feat: temporarily hide AWS Integration from integrations list

* chore: add optimized png

* refactor: extract service filter types into an enum

* chore: remove console.log

* chore: remove duplicate files

* refactor: move regions to utils

* fix: get account id from url param

* chore: address PR review comments

* refactor: use the IntegrateNowFormSections inside RegionForm

* chore: move integrations select inline style to a common class

---------

Co-authored-by: Shaheer Kochai <ashaheerki@gmail.com>
2025-01-30 20:51:49 +04:30
SagarRajput-7
3849ca1ecc feat: celery task page restructuring and other misc fixes (#6985)
* feat: celery task page restructuring and other misc fixes

* feat: added custom series hook

* feat: code fix and typo

* feat: added feedback fixes

* feat: configured error % graphs

* feat: resolved comments
2025-01-30 21:23:37 +05:30
Nityananda Gohain
d1e7cc128f fix: telemetry store (#6923)
* fix: inital changes for telemetry store

* fix: add tests and use proper config for conn

* fix: add telemetry store test

* fix: add backward compatibility for old variables and update example conf

* fix: move wrapper to telemetry store

* fix: no need to pass query for settings

* fix: remove redundant config for ch conn

* fix: use clickhouse dsn instead

* fix: update example config

* fix: update backward compatibility code

* fix: use hooks in telemetrystore

* fix: address minor comments

---------

Co-authored-by: Vibhu Pandey <vibhupandey28@gmail.com>
2025-01-30 10:21:55 +00:00
Amlan Kumar Nandy
ffd72cf406 chore: fix visibility toggle in host filters (#6976) 2025-01-30 00:15:19 +05:30
Vikrant Gupta
6dfea14219 chore(license): default to basic plan if license validation fails (#6972)
* chore(license): default to basic plan if license validation fails 3 times

* chore(license): revert the on-boot validation check

* chore(license): reset the atomic counter

* chore(license): revert the table creation removal

* chore(license): remove verify issue workflow

* chore(license): add proper log level

* chore(license): add proper log level

* chore(license): close the validation go routine post defaulting to basic plan

* chore(license): set the validator running flag to false as well
2025-01-29 18:17:50 +00:00
Shaheer Kochai
f2be856f63 fix: apply x axis date/time formatting only to panel types that have date/time in x axis (#6956) 2025-01-29 18:09:04 +00:00
Shaheer Kochai
f04589a0b2 fix: properly get the alert severity (#6974) 2025-01-29 17:30:51 +00:00
Amlan Kumar Nandy
1378590429 chore: fix filter editing issue in infra monitoring (#6961) 2025-01-29 17:21:57 +00:00
Srikanth Chekuri
88084af4d4 chore: add cluster, nodes that are sending incorrect host metrics (#6969) 2025-01-29 14:15:48 +00:00
Amlan Kumar Nandy
d0eefa0cf2 feat: add hostname and os type quick filter to hosts list (#6926) 2025-01-29 13:47:23 +00:00
Vikrant Gupta
cc9eb32c50 chore(license): clean up the older endpoints (#6971) 2025-01-29 18:13:50 +05:30
primus-bot[bot]
70169986be chore(release): bump to v0.70.0 (#6970)
#### Summary
 - Release SigNoz v0.70.0
 - Bump SigNoz OTel Collector to v0.111.25

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2025-01-29 17:49:01 +05:30
SagarRajput-7
fb3b70b729 feat: added worker count and other misc changes (#6951)
* feat: added worker count - via autocomplete API

* feat: added worker count and code fix

* feat: added lightMode styles for all celery feat

* feat: changed routes to have redirects, subroute etc

* feat: feedback changes

* feat: removed console.log
2025-01-29 17:32:01 +05:30
Prashant Shahi
36f91d1993 feat: Docker volume migration - OSS improvements Part 2 (#6962)
This is the second part of the OSS installation and improvement PR.

Users must run the migration script to migrate an existing SigNoz installation from Docker Standalone or Docker Swarm.

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-01-29 15:38:31 +05:30
Srikanth Chekuri
333f90d8ac fix: window size for small time ranges (#6964) 2025-01-29 09:17:47 +00:00
Yunus M
ea83a7e62d fix: show upgrade plan option in grace period if not converted to trial (#6909) 2025-01-29 08:16:26 +00:00
305 changed files with 26363 additions and 3025 deletions

View File

@@ -1,19 +0,0 @@
# This workflow will inspect a pull request to ensure there is a linked issue or a
# valid issue is mentioned in the body. If neither is present it fails the check and adds
# a comment alerting users of this missing requirement.
name: VerifyIssue
on:
pull_request:
types: [edited, opened]
check_run:
jobs:
verify_linked_issue:
runs-on: ubuntu-latest
name: Ensure Pull Request has a linked issue.
steps:
- name: Verify Linked Issue
uses: srikanthccv/verify-linked-issue-action@v0.71
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -77,4 +77,19 @@ apiserver:
- /api/v3/logs/livetail
logging:
excluded_routes:
- /api/v1/health
- /api/v1/health
##################### TelemetryStore #####################
telemetrystore:
# specifies the telemetrystore provider to use.
provider: clickhouse
clickhouse:
# The DSN to use for ClickHouse.
dsn: http://localhost:9000
# Maximum number of idle connections in the connection pool.
max_idle_conns: 50
# Maximum number of open connections to the database.
max_open_conns: 100
# Maximum time to wait for a connection to be established.
dial_timeout: 5s

View File

@@ -10,7 +10,7 @@ x-common: &common
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
<<: *common
!!merge <<: *common
image: clickhouse/clickhouse-server:24.1.2-alpine
tty: true
deploy:
@@ -38,7 +38,7 @@ x-clickhouse-defaults: &clickhouse-defaults
soft: 262144
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
<<: *common
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
user: root
deploy:
@@ -54,7 +54,7 @@ x-zookeeper-defaults: &zookeeper-defaults
timeout: 5s
retries: 3
x-db-depend: &db-depend
<<: *common
!!merge <<: *common
depends_on:
- clickhouse
- clickhouse-2
@@ -62,7 +62,7 @@ x-db-depend: &db-depend
- schema-migrator
services:
init-clickhouse:
<<: *common
!!merge <<: *common
image: clickhouse/clickhouse-server:24.1.2-alpine
command:
- bash
@@ -79,7 +79,7 @@ services:
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
<<: *zookeeper-defaults
!!merge <<: *zookeeper-defaults
# ports:
# - "2181:2181"
# - "2888:2888"
@@ -94,7 +94,7 @@ services:
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-2:
<<: *zookeeper-defaults
!!merge <<: *zookeeper-defaults
# ports:
# - "2182:2181"
# - "2889:2888"
@@ -109,7 +109,7 @@ services:
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-3:
<<: *zookeeper-defaults
!!merge <<: *zookeeper-defaults
# ports:
# - "2183:2181"
# - "2890:2888"
@@ -124,7 +124,7 @@ services:
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
<<: *clickhouse-defaults
!!merge <<: *clickhouse-defaults
# TODO: needed for schema-migrator to work, remove this redundancy once we have a better solution
hostname: clickhouse
# ports:
@@ -140,7 +140,7 @@ services:
- ./clickhouse-setup/data/clickhouse/:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-2:
<<: *clickhouse-defaults
!!merge <<: *clickhouse-defaults
hostname: clickhouse-2
# ports:
# - "9001:9000"
@@ -155,7 +155,7 @@ services:
- ./clickhouse-setup/data/clickhouse-2/:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-3:
<<: *clickhouse-defaults
!!merge <<: *clickhouse-defaults
hostname: clickhouse-3
# ports:
# - "9002:9000"
@@ -170,7 +170,7 @@ services:
- ./clickhouse-setup/data/clickhouse-3/:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
alertmanager:
<<: *common
!!merge <<: *common
image: signoz/alertmanager:0.23.7
command:
- --queryService.url=http://query-service:8085
@@ -180,8 +180,8 @@ services:
depends_on:
- query-service
query-service:
<<: *db-depend
image: signoz/query-service:0.69.0
!!merge <<: *db-depend
image: signoz/query-service:0.71.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -213,8 +213,8 @@ services:
timeout: 5s
retries: 3
frontend:
<<: *common
image: signoz/frontend:0.69.0
!!merge <<: *common
image: signoz/frontend:0.71.0
depends_on:
- alertmanager
- query-service
@@ -223,8 +223,8 @@ services:
volumes:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
<<: *db-depend
image: signoz/signoz-otel-collector:0.111.24
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.26
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -247,7 +247,7 @@ services:
- schema-migrator
- query-service
schema-migrator:
<<: *common
!!merge <<: *common
image: signoz/signoz-schema-migrator:0.111.24
deploy:
restart_policy:
@@ -259,7 +259,23 @@ services:
- "/signoz-schema-migrator sync --dsn=tcp://clickhouse:9000 --up= && /signoz-schema-migrator async --dsn=tcp://clickhouse:9000 --up="
depends_on:
- clickhouse
networks:
signoz-net:
name: signoz-net
volumes:
alertmanager:
name: signoz-alertmanager
clickhouse:
name: signoz-clickhouse
clickhouse-2:
name: signoz-clickhouse-2
clickhouse-3:
name: signoz-clickhouse-3
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
zookeeper-2:
name: signoz-zookeeper-2
zookeeper-3:
name: signoz-zookeeper-3

View File

@@ -10,7 +10,7 @@ x-common: &common
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
<<: *common
!!merge <<: *common
image: clickhouse/clickhouse-server:24.1.2-alpine
tty: true
deploy:
@@ -37,7 +37,7 @@ x-clickhouse-defaults: &clickhouse-defaults
soft: 262144
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
<<: *common
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
user: root
deploy:
@@ -53,13 +53,13 @@ x-zookeeper-defaults: &zookeeper-defaults
timeout: 5s
retries: 3
x-db-depend: &db-depend
<<: *common
!!merge <<: *common
depends_on:
- clickhouse
- schema-migrator
services:
init-clickhouse:
<<: *common
!!merge <<: *common
image: clickhouse/clickhouse-server:24.1.2-alpine
command:
- bash
@@ -76,13 +76,13 @@ services:
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
<<: *zookeeper-defaults
!!merge <<: *zookeeper-defaults
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-1:/bitnami/zookeeper
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ALLOW_ANONYMOUS_LOGIN=yes
@@ -90,7 +90,7 @@ services:
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
<<: *clickhouse-defaults
!!merge <<: *clickhouse-defaults
# TODO: needed for clickhouse TCP connectio
hostname: clickhouse
# ports:
@@ -103,21 +103,21 @@ services:
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
- ./clickhouse-setup/data/clickhouse/:/var/lib/clickhouse/
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
alertmanager:
<<: *common
!!merge <<: *common
image: signoz/alertmanager:0.23.7
command:
- --queryService.url=http://query-service:8085
- --storage.path=/data
volumes:
- ./clickhouse-setup/data/alertmanager:/data
- alertmanager:/data
depends_on:
- query-service
query-service:
<<: *db-depend
image: signoz/query-service:0.69.0
!!merge <<: *db-depend
image: signoz/query-service:0.71.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -128,7 +128,7 @@ services:
volumes:
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
- ../common/dashboards:/root/config/dashboards
- ./clickhouse-setup/data/signoz/:/var/lib/signoz/
- sqlite:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
@@ -149,8 +149,8 @@ services:
timeout: 5s
retries: 3
frontend:
<<: *common
image: signoz/frontend:0.69.0
!!merge <<: *common
image: signoz/frontend:0.71.0
depends_on:
- alertmanager
- query-service
@@ -159,8 +159,8 @@ services:
volumes:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
<<: *db-depend
image: signoz/signoz-otel-collector:0.111.24
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.26
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -183,7 +183,7 @@ services:
- schema-migrator
- query-service
schema-migrator:
<<: *common
!!merge <<: *common
image: signoz/signoz-schema-migrator:0.111.24
deploy:
restart_policy:
@@ -195,7 +195,15 @@ services:
- "/signoz-schema-migrator sync --dsn=tcp://clickhouse:9000 --up= && /signoz-schema-migrator async --dsn=tcp://clickhouse:9000 --up="
depends_on:
- clickhouse
networks:
signoz-net:
name: signoz-net
volumes:
alertmanager:
name: signoz-alertmanager
clickhouse:
name: signoz-clickhouse
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1

View File

@@ -1 +1 @@
COMPOSE_PROJECT_NAME=clickhouse-setup
COMPOSE_PROJECT_NAME=signoz

View File

@@ -0,0 +1,3 @@
This data directory is deprecated and will be removed in the future.
Please use the migration script under `scripts/volume-migration` to migrate data from bind mounts to Docker volumes.
The script also renames the project name to `signoz` and the network name to `signoz-net` (if not already in place).

View File

@@ -8,7 +8,7 @@ x-common: &common
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
<<: *common
!!merge <<: *common
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:24.1.2-alpine
tty: true
@@ -41,7 +41,7 @@ x-clickhouse-defaults: &clickhouse-defaults
soft: 262144
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
<<: *common
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
user: root
labels:
@@ -56,7 +56,7 @@ x-zookeeper-defaults: &zookeeper-defaults
timeout: 5s
retries: 3
x-db-depend: &db-depend
<<: *common
!!merge <<: *common
depends_on:
clickhouse:
condition: service_healthy
@@ -64,7 +64,7 @@ x-db-depend: &db-depend
condition: service_completed_successfully
services:
init-clickhouse:
<<: *common
!!merge <<: *common
image: clickhouse/clickhouse-server:24.1.2-alpine
container_name: signoz-init-clickhouse
command:
@@ -82,14 +82,14 @@ services:
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
<<: *zookeeper-defaults
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-1
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-1:/bitnami/zookeeper
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
@@ -98,14 +98,14 @@ services:
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-2:
<<: *zookeeper-defaults
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-2
# ports:
# - "2182:2181"
# - "2889:2888"
# - "3889:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-2:/bitnami/zookeeper
- zookeeper-2:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=2
- ZOO_SERVERS=zookeeper-1:2888:3888,0.0.0.0:2888:3888,zookeeper-3:2888:3888
@@ -114,14 +114,14 @@ services:
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-3:
<<: *zookeeper-defaults
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-3
# ports:
# - "2183:2181"
# - "2890:2888"
# - "3890:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-3:/bitnami/zookeeper
- zookeeper-3:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=3
- ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
@@ -130,7 +130,7 @@ services:
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
<<: *clickhouse-defaults
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse
# ports:
# - "9000:9000"
@@ -142,10 +142,10 @@ services:
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
- ./clickhouse-setup/data/clickhouse/:/var/lib/clickhouse/
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-2:
<<: *clickhouse-defaults
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse-2
# ports:
# - "9001:9000"
@@ -157,10 +157,10 @@ services:
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
- ./clickhouse-setup/data/clickhouse-2/:/var/lib/clickhouse/
- clickhouse-2:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-3:
<<: *clickhouse-defaults
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse-3
# ports:
# - "9002:9000"
@@ -172,23 +172,23 @@ services:
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
- ./clickhouse-setup/data/clickhouse-3/:/var/lib/clickhouse/
- clickhouse-3:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
alertmanager:
<<: *common
!!merge <<: *common
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
container_name: signoz-alertmanager
command:
- --queryService.url=http://query-service:8085
- --storage.path=/data
volumes:
- ./clickhouse-setup/data/alertmanager:/data
- alertmanager:/data
depends_on:
query-service:
condition: service_healthy
query-service:
<<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.69.0}
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.71.0}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -200,7 +200,7 @@ services:
volumes:
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
- ../common/dashboards:/root/config/dashboards
- ./clickhouse-setup/data/signoz/:/var/lib/signoz/
- sqlite:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
@@ -221,8 +221,8 @@ services:
timeout: 5s
retries: 3
frontend:
<<: *common
image: signoz/frontend:${DOCKER_TAG:-0.69.0}
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.71.0}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -233,8 +233,8 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
<<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24}
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.26}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -259,7 +259,7 @@ services:
query-service:
condition: service_healthy
schema-migrator-sync:
<<: *common
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-sync
command:
@@ -270,14 +270,30 @@ services:
clickhouse:
condition: service_healthy
schema-migrator-async:
<<: *db-depend
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-async
command:
- async
- --dsn=tcp://clickhouse:9000
- --up=
networks:
signoz-net:
name: signoz-net
volumes:
alertmanager:
name: signoz-alertmanager
clickhouse:
name: signoz-clickhouse
clickhouse-2:
name: signoz-clickhouse-2
clickhouse-3:
name: signoz-clickhouse-3
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
zookeeper-2:
name: signoz-zookeeper-2
zookeeper-3:
name: signoz-zookeeper-3

View File

@@ -8,7 +8,7 @@ x-common: &common
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
<<: *common
!!merge <<: *common
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:24.1.2-alpine
tty: true
@@ -37,7 +37,7 @@ x-clickhouse-defaults: &clickhouse-defaults
soft: 262144
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
<<: *common
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
user: root
labels:
@@ -52,7 +52,7 @@ x-zookeeper-defaults: &zookeeper-defaults
timeout: 5s
retries: 3
x-db-depend: &db-depend
<<: *common
!!merge <<: *common
depends_on:
clickhouse:
condition: service_healthy
@@ -60,7 +60,7 @@ x-db-depend: &db-depend
condition: service_completed_successfully
services:
init-clickhouse:
<<: *common
!!merge <<: *common
image: clickhouse/clickhouse-server:24.1.2-alpine
container_name: signoz-init-clickhouse
command:
@@ -78,14 +78,14 @@ services:
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
<<: *zookeeper-defaults
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-1
ports:
- "2181:2181"
- "2888:2888"
- "3888:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-1:/bitnami/zookeeper
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ALLOW_ANONYMOUS_LOGIN=yes
@@ -93,7 +93,7 @@ services:
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
<<: *clickhouse-defaults
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse
ports:
- "9000:9000"
@@ -105,23 +105,23 @@ services:
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
- ./clickhouse-setup/data/clickhouse/:/var/lib/clickhouse/
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
alertmanager:
<<: *common
!!merge <<: *common
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
container_name: signoz-alertmanager
command:
- --queryService.url=http://query-service:8085
- --storage.path=/data
volumes:
- ./clickhouse-setup/data/alertmanager:/data
- alertmanager:/data
depends_on:
query-service:
condition: service_healthy
query-service:
<<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.69.0}
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.71.0}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -134,7 +134,7 @@ services:
volumes:
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
- ../common/dashboards:/root/config/dashboards
- ./clickhouse-setup/data/signoz/:/var/lib/signoz/
- sqlite:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
@@ -156,8 +156,8 @@ services:
timeout: 5s
retries: 3
frontend:
<<: *common
image: signoz/frontend:${DOCKER_TAG:-0.69.0}
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.71.0}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -167,8 +167,8 @@ services:
volumes:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
<<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24}
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.26}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -189,7 +189,7 @@ services:
query-service:
condition: service_healthy
schema-migrator-sync:
<<: *common
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-sync
command:
@@ -200,14 +200,22 @@ services:
clickhouse:
condition: service_healthy
schema-migrator-async:
<<: *db-depend
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-async
command:
- async
- --dsn=tcp://clickhouse:9000
- --up=
networks:
signoz-net:
name: signoz-net
volumes:
alertmanager:
name: signoz-alertmanager
clickhouse:
name: signoz-clickhouse
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1

View File

@@ -8,7 +8,7 @@ x-common: &common
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
<<: *common
!!merge <<: *common
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:24.1.2-alpine
tty: true
@@ -37,7 +37,7 @@ x-clickhouse-defaults: &clickhouse-defaults
soft: 262144
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
<<: *common
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
user: root
labels:
@@ -52,7 +52,7 @@ x-zookeeper-defaults: &zookeeper-defaults
timeout: 5s
retries: 3
x-db-depend: &db-depend
<<: *common
!!merge <<: *common
depends_on:
clickhouse:
condition: service_healthy
@@ -60,7 +60,7 @@ x-db-depend: &db-depend
condition: service_completed_successfully
services:
init-clickhouse:
<<: *common
!!merge <<: *common
image: clickhouse/clickhouse-server:24.1.2-alpine
container_name: signoz-init-clickhouse
command:
@@ -78,14 +78,14 @@ services:
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
<<: *zookeeper-defaults
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-1
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-1:/bitnami/zookeeper
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ALLOW_ANONYMOUS_LOGIN=yes
@@ -93,7 +93,7 @@ services:
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
<<: *clickhouse-defaults
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse
# ports:
# - "9000:9000"
@@ -105,23 +105,23 @@ services:
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
- ./clickhouse-setup/data/clickhouse/:/var/lib/clickhouse/
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
alertmanager:
<<: *common
!!merge <<: *common
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
container_name: signoz-alertmanager
command:
- --queryService.url=http://query-service:8085
- --storage.path=/data
volumes:
- ./clickhouse-setup/data/alertmanager:/data
- alertmanager:/data
depends_on:
query-service:
condition: service_healthy
query-service:
<<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.69.0}
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.71.0}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -133,7 +133,7 @@ services:
volumes:
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
- ../common/dashboards:/root/config/dashboards
- ./clickhouse-setup/data/signoz/:/var/lib/signoz/
- sqlite:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
@@ -154,8 +154,8 @@ services:
timeout: 5s
retries: 3
frontend:
<<: *common
image: signoz/frontend:${DOCKER_TAG:-0.69.0}
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.71.0}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -165,8 +165,8 @@ services:
volumes:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
<<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24}
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.26}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -187,7 +187,7 @@ services:
query-service:
condition: service_healthy
schema-migrator-sync:
<<: *common
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-sync
command:
@@ -198,14 +198,22 @@ services:
clickhouse:
condition: service_healthy
schema-migrator-async:
<<: *db-depend
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-async
command:
- async
- --dsn=tcp://clickhouse:9000
- --up=
networks:
signoz-net:
name: signoz-net
volumes:
alertmanager:
name: signoz-alertmanager
clickhouse:
name: signoz-clickhouse
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1

View File

@@ -59,7 +59,7 @@ type anomalyQueryParams struct {
// The results obtained from this query are used to compare with predicted values
// and to detect anomalies
CurrentPeriodQuery *v3.QueryRangeParamsV3
// PastPeriodQuery is the query range params for past seasonal period
// PastPeriodQuery is the query range params for past period of seasonality
// Example: For weekly seasonality, (now-1w-5m, now-1w)
// : For daily seasonality, (now-1d-5m, now-1d)
// : For hourly seasonality, (now-1h-5m, now-1h)
@@ -74,7 +74,6 @@ type anomalyQueryParams struct {
// : For daily seasonality, this is the query range params for the (now-2d-5m, now-1d)
// : For hourly seasonality, this is the query range params for the (now-2h-5m, now-1h)
PastSeasonQuery *v3.QueryRangeParamsV3
// Past2SeasonQuery is the query range params for past 2 seasonal period to the current season
// Example: For weekly seasonality, this is the query range params for the (now-3w-5m, now-2w)
// : For daily seasonality, this is the query range params for the (now-3d-5m, now-2d)
@@ -144,13 +143,13 @@ func prepareAnomalyQueryParams(req *v3.QueryRangeParamsV3, seasonality Seasonali
switch seasonality {
case SeasonalityWeekly:
currentGrowthPeriodStart = start - oneWeekOffset
currentGrowthPeriodEnd = end
currentGrowthPeriodEnd = start
case SeasonalityDaily:
currentGrowthPeriodStart = start - oneDayOffset
currentGrowthPeriodEnd = end
currentGrowthPeriodEnd = start
case SeasonalityHourly:
currentGrowthPeriodStart = start - oneHourOffset
currentGrowthPeriodEnd = end
currentGrowthPeriodEnd = start
}
currentGrowthQuery := &v3.QueryRangeParamsV3{

View File

@@ -194,10 +194,11 @@ func (p *BaseSeasonalProvider) getMovingAvg(series *v3.Series, movingAvgWindowSi
}
var sum float64
points := series.Points[startIdx:]
for i := 0; i < movingAvgWindowSize && i < len(points); i++ {
windowSize := int(math.Min(float64(movingAvgWindowSize), float64(len(points))))
for i := 0; i < windowSize; i++ {
sum += points[i].Value
}
avg := sum / float64(movingAvgWindowSize)
avg := sum / float64(windowSize)
return avg
}
@@ -226,21 +227,25 @@ func (p *BaseSeasonalProvider) getPredictedSeries(
// plus the average of the current season series
// minus the mean of the past season series, past2 season series and past3 season series
for idx, curr := range series.Points {
predictedValue :=
p.getMovingAvg(prevSeries, movingAvgWindowSize, idx) +
p.getAvg(currentSeasonSeries) -
p.getMean(p.getAvg(pastSeasonSeries), p.getAvg(past2SeasonSeries), p.getAvg(past3SeasonSeries))
movingAvg := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
avg := p.getAvg(currentSeasonSeries)
mean := p.getMean(p.getAvg(pastSeasonSeries), p.getAvg(past2SeasonSeries), p.getAvg(past3SeasonSeries))
predictedValue := movingAvg + avg - mean
if predictedValue < 0 {
// this should not happen (except when the data has extreme outliers)
// we will use the moving avg of the previous period series in this case
zap.L().Warn("predictedValue is less than 0", zap.Float64("predictedValue", predictedValue), zap.Any("labels", series.Labels))
predictedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
}
zap.L().Info("predictedSeries",
zap.Float64("movingAvg", p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)),
zap.Float64("avg", p.getAvg(currentSeasonSeries)),
zap.Float64("mean", p.getMean(p.getAvg(pastSeasonSeries), p.getAvg(past2SeasonSeries), p.getAvg(past3SeasonSeries))),
zap.L().Debug("predictedSeries",
zap.Float64("movingAvg", movingAvg),
zap.Float64("avg", avg),
zap.Float64("mean", mean),
zap.Any("labels", series.Labels),
zap.Float64("predictedValue", predictedValue),
zap.Float64("curr", curr.Value),
)
predictedSeries.Points = append(predictedSeries.Points, v3.Point{
Timestamp: curr.Timestamp,

View File

@@ -26,9 +26,6 @@ type APIHandlerOptions struct {
DataConnector interfaces.DataConnector
SkipConfig *basemodel.SkipConfig
PreferSpanMetrics bool
MaxIdleConns int
MaxOpenConns int
DialTimeout time.Duration
AppDao dao.ModelDao
RulesManager *rules.Manager
UsageManager *usage.Manager
@@ -39,6 +36,7 @@ type APIHandlerOptions struct {
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
Cache cache.Cache
Gateway *httputil.ReverseProxy
GatewayUrl string
// Querier Influx Interval
FluxInterval time.Duration
UseLogsNewSchema bool
@@ -57,9 +55,6 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
Reader: opts.DataConnector,
SkipConfig: opts.SkipConfig,
PreferSpanMetrics: opts.PreferSpanMetrics,
MaxIdleConns: opts.MaxIdleConns,
MaxOpenConns: opts.MaxOpenConns,
DialTimeout: opts.DialTimeout,
AppDao: opts.AppDao,
RuleManager: opts.RulesManager,
FeatureFlags: opts.FeatureFlags,
@@ -117,13 +112,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
// note: add ee override methods first
// routes available only in ee version
router.HandleFunc("/api/v1/licenses",
am.AdminAccess(ah.listLicenses)).
Methods(http.MethodGet)
router.HandleFunc("/api/v1/licenses",
am.AdminAccess(ah.applyLicense)).
Methods(http.MethodPost)
router.HandleFunc("/api/v1/featureFlags",
am.OpenAccess(ah.getFeatureFlags)).
@@ -178,11 +166,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v2
router.HandleFunc("/api/v2/licenses",
am.ViewAccess(ah.listLicensesV2)).
Methods(http.MethodGet)
// v3
router.HandleFunc("/api/v3/licenses", am.ViewAccess(ah.listLicensesV3)).Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.applyLicenseV3)).Methods(http.MethodPost)
@@ -199,6 +182,17 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
}
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *baseapp.AuthMiddleware) {
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
router.HandleFunc(
"/api/v1/cloud-integrations/{cloudProvider}/accounts/generate-connection-params",
am.EditAccess(ah.CloudIntegrationsGenerateConnectionParams),
).Methods(http.MethodGet)
}
func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
version := version.GetVersion()
versionResponse := basemodel.GetVersionResponse{

View File

@@ -0,0 +1,425 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model"
"go.signoz.io/signoz/pkg/query-service/auth"
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/dao"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
type CloudIntegrationConnectionParamsResponse struct {
IngestionUrl string `json:"ingestion_url,omitempty"`
IngestionKey string `json:"ingestion_key,omitempty"`
SigNozAPIUrl string `json:"signoz_api_url,omitempty"`
SigNozAPIKey string `json:"signoz_api_key,omitempty"`
}
func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseWriter, r *http.Request) {
cloudProvider := mux.Vars(r)["cloudProvider"]
if cloudProvider != "aws" {
RespondError(w, basemodel.BadRequest(fmt.Errorf(
"cloud provider not supported: %s", cloudProvider,
)), nil)
return
}
currentUser, err := auth.GetUserFromRequest(r)
if err != nil {
RespondError(w, basemodel.UnauthorizedError(fmt.Errorf(
"couldn't deduce current user: %w", err,
)), nil)
return
}
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), currentUser.OrgId, cloudProvider)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't provision PAT for cloud integration:",
), nil)
return
}
result := CloudIntegrationConnectionParamsResponse{
SigNozAPIKey: apiKey,
}
license, apiErr := ah.LM().GetRepo().GetActiveLicense(r.Context())
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't look for active license",
), nil)
return
}
if license == nil {
// Return the API Key (PAT) even if the rest of the params can not be deduced.
// Params not returned from here will be requested from the user via form inputs.
// This enables gracefully degraded but working experience even for non-cloud deployments.
zap.L().Info("ingestion params and signoz api url can not be deduced since no license was found")
ah.Respond(w, result)
return
}
ingestionUrl, signozApiUrl, apiErr := getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't deduce ingestion url and signoz api url",
), nil)
return
}
result.IngestionUrl = ingestionUrl
result.SigNozAPIUrl = signozApiUrl
gatewayUrl := ah.opts.GatewayUrl
if len(gatewayUrl) > 0 {
ingestionKey, apiErr := getOrCreateCloudProviderIngestionKey(
r.Context(), gatewayUrl, license.Key, cloudProvider,
)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't get or create ingestion key",
), nil)
return
}
result.IngestionKey = ingestionKey
} else {
zap.L().Info("ingestion key can't be deduced since no gateway url has been configured")
}
ah.Respond(w, result)
}
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) (
string, *basemodel.ApiError,
) {
integrationPATName := fmt.Sprintf("%s integration", cloudProvider)
integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
if apiErr != nil {
return "", apiErr
}
allPats, err := ah.AppDao().ListPATs(ctx)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't list PATs: %w", err.Error(),
))
}
for _, p := range allPats {
if p.UserID == integrationUser.Id && p.Name == integrationPATName {
return p.Token, nil
}
}
zap.L().Info(
"no PAT found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
)
newPAT := model.PAT{
Token: generatePATToken(),
UserID: integrationUser.Id,
Name: integrationPATName,
Role: baseconstants.ViewerGroup,
ExpiresAt: 0,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
integrationPAT, err := ah.AppDao().CreatePAT(ctx, newPAT)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err.Error(),
))
}
return integrationPAT.Token, nil
}
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
ctx context.Context, orgId string, cloudProvider string,
) (*basemodel.User, *basemodel.ApiError) {
cloudIntegrationUserId := fmt.Sprintf("%s-integration", cloudProvider)
integrationUserResult, apiErr := ah.AppDao().GetUser(ctx, cloudIntegrationUserId)
if apiErr != nil {
return nil, basemodel.WrapApiError(apiErr, "couldn't look for integration user")
}
if integrationUserResult != nil {
return &integrationUserResult.User, nil
}
zap.L().Info(
"cloud integration user not found. Attempting to create the user",
zap.String("cloudProvider", cloudProvider),
)
newUser := &basemodel.User{
Id: cloudIntegrationUserId,
Name: fmt.Sprintf("%s integration", cloudProvider),
Email: fmt.Sprintf("%s@signoz.io", cloudIntegrationUserId),
CreatedAt: time.Now().Unix(),
OrgId: orgId,
}
viewerGroup, apiErr := dao.DB().GetGroupByName(ctx, baseconstants.ViewerGroup)
if apiErr != nil {
return nil, basemodel.WrapApiError(apiErr, "couldn't get viewer group for creating integration user")
}
newUser.GroupId = viewerGroup.Id
passwordHash, err := auth.PasswordHash(uuid.NewString())
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't hash random password for cloud integration user: %w", err,
))
}
newUser.Password = passwordHash
integrationUser, apiErr := ah.AppDao().CreateUser(ctx, newUser, false)
if apiErr != nil {
return nil, basemodel.WrapApiError(apiErr, "couldn't create cloud integration user")
}
return integrationUser, nil
}
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
string, string, *basemodel.ApiError,
) {
url := fmt.Sprintf(
"%s%s",
strings.TrimSuffix(constants.ZeusURL, "/"),
"/v2/deployments/me",
)
type deploymentResponse struct {
Status string `json:"status"`
Error string `json:"error"`
Data struct {
Name string `json:"name"`
ClusterInfo struct {
Region struct {
DNS string `json:"dns"`
} `json:"region"`
} `json:"cluster"`
} `json:"data"`
}
resp, apiErr := requestAndParseResponse[deploymentResponse](
ctx, url, map[string]string{"X-Signoz-Cloud-Api-Key": licenseKey}, nil,
)
if apiErr != nil {
return "", "", basemodel.WrapApiError(
apiErr, "couldn't query for deployment info",
)
}
if resp.Status != "success" {
return "", "", basemodel.InternalError(fmt.Errorf(
"couldn't query for deployment info: status: %s, error: %s",
resp.Status, resp.Error,
))
}
regionDns := resp.Data.ClusterInfo.Region.DNS
deploymentName := resp.Data.Name
if len(regionDns) < 1 || len(deploymentName) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", "", basemodel.InternalError(fmt.Errorf(
"deployment info response not in expected shape. couldn't determine region dns and deployment name",
))
}
ingestionUrl := fmt.Sprintf("https://ingest.%s", regionDns)
signozApiUrl := fmt.Sprintf("https://%s.%s", deploymentName, regionDns)
return ingestionUrl, signozApiUrl, nil
}
type ingestionKey struct {
Name string `json:"name"`
Value string `json:"value"`
// other attributes from gateway response not included here since they are not being used.
}
type ingestionKeysSearchResponse struct {
Status string `json:"status"`
Data []ingestionKey `json:"data"`
Error string `json:"error"`
}
type createIngestionKeyResponse struct {
Status string `json:"status"`
Data ingestionKey `json:"data"`
Error string `json:"error"`
}
func getOrCreateCloudProviderIngestionKey(
ctx context.Context, gatewayUrl string, licenseKey string, cloudProvider string,
) (string, *basemodel.ApiError) {
cloudProviderKeyName := fmt.Sprintf("%s-integration", cloudProvider)
// see if the key already exists
searchResult, apiErr := requestGateway[ingestionKeysSearchResponse](
ctx,
gatewayUrl,
licenseKey,
fmt.Sprintf("/v1/workspaces/me/keys/search?name=%s", cloudProviderKeyName),
nil,
)
if apiErr != nil {
return "", basemodel.WrapApiError(
apiErr, "couldn't search for cloudprovider ingestion key",
)
}
if searchResult.Status != "success" {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't search for cloudprovider ingestion key: status: %s, error: %s",
searchResult.Status, searchResult.Error,
))
}
for _, k := range searchResult.Data {
if k.Name == cloudProviderKeyName {
if len(k.Value) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", basemodel.InternalError(fmt.Errorf(
"ingestion keys search response not as expected",
))
}
return k.Value, nil
}
}
zap.L().Info(
"no existing ingestion key found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
)
createKeyResult, apiErr := requestGateway[createIngestionKeyResponse](
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
map[string]any{
"name": cloudProviderKeyName,
"tags": []string{"integration", cloudProvider},
},
)
if apiErr != nil {
return "", basemodel.WrapApiError(
apiErr, "couldn't create cloudprovider ingestion key",
)
}
if createKeyResult.Status != "success" {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloudprovider ingestion key: status: %s, error: %s",
createKeyResult.Status, createKeyResult.Error,
))
}
ingestionKey := createKeyResult.Data.Value
if len(ingestionKey) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", basemodel.InternalError(fmt.Errorf(
"ingestion key creation response not as expected",
))
}
return ingestionKey, nil
}
func requestGateway[ResponseType any](
ctx context.Context, gatewayUrl string, licenseKey string, path string, payload any,
) (*ResponseType, *basemodel.ApiError) {
baseUrl := strings.TrimSuffix(gatewayUrl, "/")
reqUrl := fmt.Sprintf("%s%s", baseUrl, path)
headers := map[string]string{
"X-Signoz-Cloud-Api-Key": licenseKey,
"X-Consumer-Username": "lid:00000000-0000-0000-0000-000000000000",
"X-Consumer-Groups": "ns:default",
}
return requestAndParseResponse[ResponseType](ctx, reqUrl, headers, payload)
}
func requestAndParseResponse[ResponseType any](
ctx context.Context, url string, headers map[string]string, payload any,
) (*ResponseType, *basemodel.ApiError) {
reqMethod := http.MethodGet
var reqBody io.Reader
if payload != nil {
reqMethod = http.MethodPost
bodyJson, err := json.Marshal(payload)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't serialize request payload to JSON: %w", err,
))
}
reqBody = bytes.NewBuffer([]byte(bodyJson))
}
req, err := http.NewRequestWithContext(ctx, reqMethod, url, reqBody)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't prepare request: %w", err,
))
}
for k, v := range headers {
req.Header.Set(k, v)
}
client := &http.Client{
Timeout: 10 * time.Second,
}
response, err := client.Do(req)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't make request: %w", err))
}
defer response.Body.Close()
respBody, err := io.ReadAll(response.Body)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't read response: %w", err))
}
var resp ResponseType
err = json.Unmarshal(respBody, &resp)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't unmarshal gateway response into %T", resp,
))
}
return &resp, nil
}

View File

@@ -1,7 +1,6 @@
package api
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -64,55 +63,8 @@ type ApplyLicenseRequest struct {
LicenseKey string `json:"key"`
}
type ListLicenseResponse map[string]interface{}
func convertLicenseV3ToListLicenseResponse(licensesV3 []*model.LicenseV3) []ListLicenseResponse {
listLicenses := []ListLicenseResponse{}
for _, license := range licensesV3 {
listLicenses = append(listLicenses, license.Data)
}
return listLicenses
}
func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicenses(context.Background())
if apiError != nil {
RespondError(w, apiError, nil)
}
ah.Respond(w, licenses)
}
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
var l model.License
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
if l.Key == "" {
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
return
}
license, apiError := ah.LM().ActivateV3(r.Context(), l.Key)
if apiError != nil {
RespondError(w, apiError, nil)
return
}
ah.Respond(w, license)
}
func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicensesV3(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
ah.Respond(w, convertLicenseV3ToListLicenseResponse(licenses))
ah.listLicensesV2(w, r)
}
func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) {
@@ -121,6 +73,7 @@ func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request)
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
// return 404 not found if there is no active license
if activeLicense == nil {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no active license found")}, nil)

View File

@@ -20,22 +20,20 @@ type ClickhouseReader struct {
func NewDataConnector(
localDB *sqlx.DB,
ch clickhouse.Conn,
promConfigPath string,
lm interfaces.FeatureLookup,
maxIdleConns int,
maxOpenConns int,
dialTimeout time.Duration,
cluster string,
useLogsNewSchema bool,
useTraceNewSchema bool,
fluxIntervalForTraceDetail time.Duration,
cache cache.Cache,
) *ClickhouseReader {
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
chReader := basechr.NewReader(localDB, ch, promConfigPath, lm, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
return &ClickhouseReader{
conn: ch.GetConn(),
conn: ch,
appdb: localDB,
ClickHouseReader: ch,
ClickHouseReader: chReader,
}
}

View File

@@ -11,7 +11,6 @@ import (
"net"
"net/http"
_ "net/http/pprof" // http profiler
"os"
"regexp"
"time"
@@ -74,9 +73,6 @@ type ServerOptions struct {
DisableRules bool
RuleRepoURL string
PreferSpanMetrics bool
MaxIdleConns int
MaxOpenConns int
DialTimeout time.Duration
CacheConfigPath string
FluxInterval string
FluxIntervalForTraceDetail string
@@ -152,27 +148,20 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
var reader interfaces.DataConnector
storage := os.Getenv("STORAGE")
if storage == "clickhouse" {
zap.L().Info("Using ClickHouse as datastore ...")
qb := db.NewDataConnector(
serverOptions.SigNoz.SQLStore.SQLxDB(),
serverOptions.PromConfigPath,
lm,
serverOptions.MaxIdleConns,
serverOptions.MaxOpenConns,
serverOptions.DialTimeout,
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
)
go qb.Start(readerReady)
reader = qb
} else {
return nil, fmt.Errorf("storage type: %s is not supported in query service", storage)
}
qb := db.NewDataConnector(
serverOptions.SigNoz.SQLStore.SQLxDB(),
serverOptions.SigNoz.TelemetryStore.ClickHouseDB(),
serverOptions.PromConfigPath,
lm,
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
)
go qb.Start(readerReady)
reader = qb
skipConfig := &basemodel.SkipConfig{}
if serverOptions.SkipTopLvlOpsPath != "" {
// read skip config
@@ -245,7 +234,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// start the usagemanager
usageManager, err := usage.New(modelDao, lm.GetRepo(), reader.GetConn())
usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickHouseDB(), serverOptions.Config.TelemetryStore.ClickHouse.DSN)
if err != nil {
return nil, err
}
@@ -266,9 +255,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
DataConnector: reader,
SkipConfig: skipConfig,
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
MaxIdleConns: serverOptions.MaxIdleConns,
MaxOpenConns: serverOptions.MaxOpenConns,
DialTimeout: serverOptions.DialTimeout,
AppDao: modelDao,
RulesManager: rm,
UsageManager: usageManager,
@@ -280,6 +266,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
Cache: c,
FluxInterval: fluxInterval,
Gateway: gatewayProxy,
GatewayUrl: serverOptions.GatewayUrl,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
}

View File

@@ -7,7 +7,6 @@ import (
basedao "go.signoz.io/signoz/pkg/query-service/dao"
basedsql "go.signoz.io/signoz/pkg/query-service/dao/sqlite"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
"go.uber.org/zap"
)
type modelDao struct {
@@ -29,41 +28,6 @@ func (m *modelDao) checkFeature(key string) error {
return m.flags.CheckFeature(key)
}
func columnExists(db *sqlx.DB, tableName, columnName string) bool {
query := fmt.Sprintf("PRAGMA table_info(%s);", tableName)
rows, err := db.Query(query)
if err != nil {
zap.L().Error("Failed to query table info", zap.Error(err))
return false
}
defer rows.Close()
var (
cid int
name string
ctype string
notnull int
dflt_value *string
pk int
)
for rows.Next() {
err := rows.Scan(&cid, &name, &ctype, &notnull, &dflt_value, &pk)
if err != nil {
zap.L().Error("Failed to scan table info", zap.Error(err))
return false
}
if name == columnName {
return true
}
}
err = rows.Err()
if err != nil {
zap.L().Error("Failed to scan table info", zap.Error(err))
return false
}
return false
}
// InitDB creates and extends base model DB repository
func InitDB(inputDB *sqlx.DB) (*modelDao, error) {
dao, err := basedsql.InitDB(inputDB)
@@ -73,69 +37,6 @@ func InitDB(inputDB *sqlx.DB) (*modelDao, error) {
// set package variable so dependent base methods (e.g. AuthCache) will work
basedao.SetDB(dao)
m := &modelDao{ModelDaoSqlite: dao}
table_schema := `
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS org_domains(
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
name VARCHAR(50) NOT NULL UNIQUE,
created_at INTEGER NOT NULL,
updated_at INTEGER,
data TEXT NOT NULL,
FOREIGN KEY(org_id) REFERENCES organizations(id)
);
CREATE TABLE IF NOT EXISTS personal_access_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
user_id TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_used INTEGER NOT NULL,
revoked BOOLEAN NOT NULL,
updated_by_user_id TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
`
_, err = m.DB().Exec(table_schema)
if err != nil {
return nil, fmt.Errorf("error in creating tables: %v", err.Error())
}
if !columnExists(m.DB(), "personal_access_tokens", "role") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN role TEXT NOT NULL DEFAULT 'ADMIN';")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "updated_at") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0;")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "last_used") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "revoked") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN revoked BOOLEAN NOT NULL DEFAULT FALSE;")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "updated_by_user_id") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN updated_by_user_id TEXT NOT NULL DEFAULT '';")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
return m, nil
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"go.signoz.io/signoz/ee/query-service/license/sqlite"
"go.signoz.io/signoz/ee/query-service/model"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
@@ -28,23 +27,6 @@ func NewLicenseRepo(db *sqlx.DB) Repo {
}
}
func (r *Repo) InitDB(inputDB *sqlx.DB) error {
return sqlite.InitDB(inputDB)
}
func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) {
licenses := []model.License{}
query := "SELECT key, activationId, planDetails, validationMessage FROM licenses"
err := r.db.Select(&licenses, query)
if err != nil {
return nil, fmt.Errorf("failed to get licenses from db: %v", err)
}
return licenses, nil
}
func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
licensesData := []model.LicenseDB{}
licenseV3Data := []*model.LicenseV3{}
@@ -73,35 +55,6 @@ func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
return licenseV3Data, nil
}
func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemodel.ApiError) {
var err error
licenses := []model.License{}
query := "SELECT key, activationId, planDetails, validationMessage FROM licenses"
err = r.db.Select(&licenses, query)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
var active *model.License
for _, l := range licenses {
l.ParsePlan()
if active == nil &&
(l.ValidFrom != 0) &&
(l.ValidUntil == -1 || l.ValidUntil > time.Now().Unix()) {
active = &l
}
if active != nil &&
l.ValidFrom > active.ValidFrom &&
(l.ValidUntil == -1 || l.ValidUntil > time.Now().Unix()) {
active = &l
}
}
return active, nil
}
// GetActiveLicense fetches the latest active license from DB.
// If the license is not present, expect a nil license and a nil error in the output.
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
@@ -156,50 +109,56 @@ func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error)
return active, nil
}
// InsertLicense inserts a new license in db
func (r *Repo) InsertLicense(ctx context.Context, l *model.License) error {
// InsertLicenseV3 inserts a new license v3 in db
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
if l.Key == "" {
return fmt.Errorf("insert license failed: license key is required")
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
// licsense is the entity of zeus so putting the entire license here without defining schema
licenseData, err := json.Marshal(l.Data)
if err != nil {
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
}
query := `INSERT INTO licenses
(key, planDetails, activationId, validationmessage)
VALUES ($1, $2, $3, $4)`
_, err := r.db.ExecContext(ctx,
_, err = r.db.ExecContext(ctx,
query,
l.ID,
l.Key,
l.PlanDetails,
l.ActivationId,
l.ValidationMessage)
string(licenseData),
)
if err != nil {
if sqliteErr, ok := err.(sqlite3.Error); ok {
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
}
}
zap.L().Error("error in inserting license data: ", zap.Error(err))
return fmt.Errorf("failed to insert license in db: %v", err)
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
}
return nil
}
// UpdatePlanDetails writes new plan details to the db
func (r *Repo) UpdatePlanDetails(ctx context.Context,
key,
planDetails string) error {
// UpdateLicenseV3 updates a new license v3 in db
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
if key == "" {
return fmt.Errorf("update plan details failed: license key is required")
// the key and id for the license can't change so only update the data here!
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
license, err := json.Marshal(l.Data)
if err != nil {
return fmt.Errorf("insert license failed: license marshal error")
}
query := `UPDATE licenses
SET planDetails = $1,
updatedAt = $2
WHERE key = $3`
_, err := r.db.ExecContext(ctx, query, planDetails, time.Now(), key)
_, err = r.db.ExecContext(ctx,
query,
license,
l.ID,
)
if err != nil {
zap.L().Error("error in updating license: ", zap.Error(err))
zap.L().Error("error in updating license data: ", zap.Error(err))
return fmt.Errorf("failed to update license in db: %v", err)
}
@@ -281,59 +240,3 @@ func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
}
return nil
}
// InsertLicenseV3 inserts a new license v3 in db
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
// licsense is the entity of zeus so putting the entire license here without defining schema
licenseData, err := json.Marshal(l.Data)
if err != nil {
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
}
_, err = r.db.ExecContext(ctx,
query,
l.ID,
l.Key,
string(licenseData),
)
if err != nil {
if sqliteErr, ok := err.(sqlite3.Error); ok {
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
}
}
zap.L().Error("error in inserting license data: ", zap.Error(err))
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
}
return nil
}
// UpdateLicenseV3 updates a new license v3 in db
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
// the key and id for the license can't change so only update the data here!
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
license, err := json.Marshal(l.Data)
if err != nil {
return fmt.Errorf("insert license failed: license marshal error")
}
_, err = r.db.ExecContext(ctx,
query,
license,
l.ID,
)
if err != nil {
zap.L().Error("error in updating license data: ", zap.Error(err))
return fmt.Errorf("failed to update license in db: %v", err)
}
return nil
}

View File

@@ -2,7 +2,6 @@ package license
import (
"context"
"fmt"
"sync/atomic"
"time"
@@ -27,26 +26,19 @@ var LM *Manager
var validationFrequency = 24 * 60 * time.Minute
type Manager struct {
repo *Repo
mutex sync.Mutex
repo *Repo
mutex sync.Mutex
validatorRunning bool
// end the license validation, this is important to gracefully
// stopping validation and protect in-consistent updates
done chan struct{}
// terminated waits for the validate go routine to end
terminated chan struct{}
// last time the license was validated
lastValidated int64
// keep track of validation failure attempts
failedAttempts uint64
// keep track of active license and features
activeLicense *model.License
activeLicenseV3 *model.LicenseV3
activeFeatures basemodel.FeatureSet
}
@@ -57,19 +49,13 @@ func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error)
}
repo := NewLicenseRepo(db)
err := repo.InitDB(db)
if err != nil {
return nil, fmt.Errorf("failed to initiate license repo: %v", err)
}
m := &Manager{
repo: &repo,
}
if err := m.start(features...); err != nil {
return m, err
}
LM = m
return m, nil
}
@@ -119,6 +105,7 @@ func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
if err != nil {
return err
}
if active != nil {
lm.SetActiveV3(active, features...)
} else {
@@ -136,32 +123,6 @@ func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
return nil
}
func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, apiError *model.ApiError) {
licenses, err := lm.repo.GetLicenses(ctx)
if err != nil {
return nil, model.InternalError(err)
}
for _, l := range licenses {
l.ParsePlan()
if lm.activeLicense != nil && l.Key == lm.activeLicense.Key {
l.IsCurrent = true
}
if l.ValidUntil == -1 {
// for subscriptions, there is no end-date as such
// but for showing user some validity we default one year timespan
l.ValidUntil = l.ValidFrom + 31556926
}
response = append(response, l)
}
return
}
func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) {
licenses, err := lm.repo.GetLicensesV3(ctx)
@@ -188,11 +149,11 @@ func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.License
func (lm *Manager) ValidatorV3(ctx context.Context) {
zap.L().Info("ValidatorV3 started!")
defer close(lm.terminated)
tick := time.NewTicker(validationFrequency)
defer tick.Stop()
lm.ValidateV3(ctx)
for {
select {
case <-lm.done:
@@ -238,10 +199,27 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
lm.lastValidated = time.Now().Unix()
if reterr != nil {
zap.L().Error("License validation completed with error", zap.Error(reterr))
atomic.AddUint64(&lm.failedAttempts, 1)
// default to basic plan if validation fails for three consecutive times
if atomic.LoadUint64(&lm.failedAttempts) > 3 {
zap.L().Error("License validation completed with error for three consecutive times, defaulting to basic plan", zap.String("license_id", lm.activeLicenseV3.ID), zap.Bool("license_validation", false))
lm.activeLicenseV3 = nil
lm.activeFeatures = model.BasicPlan
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Error("Couldn't initialize features", zap.Error(err))
}
lm.done <- struct{}{}
lm.validatorRunning = false
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
map[string]interface{}{"err": reterr.Error()}, "", true, false)
} else {
// reset the failed attempts counter
atomic.StoreUint64(&lm.failedAttempts, 0)
zap.L().Info("License validation completed with no errors")
}

View File

@@ -1,63 +0,0 @@
package sqlite
import (
"fmt"
"github.com/jmoiron/sqlx"
)
func InitDB(db *sqlx.DB) error {
var err error
if db == nil {
return fmt.Errorf("invalid db connection")
}
table_schema := `CREATE TABLE IF NOT EXISTS licenses(
key TEXT PRIMARY KEY,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
planDetails TEXT,
activationId TEXT,
validationMessage TEXT,
lastValidated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS sites(
uuid TEXT PRIMARY KEY,
alias VARCHAR(180) DEFAULT 'PROD',
url VARCHAR(300),
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating licenses table: %s", err.Error())
}
table_schema = `CREATE TABLE IF NOT EXISTS feature_status (
name TEXT PRIMARY KEY,
active bool,
usage INTEGER DEFAULT 0,
usage_limit INTEGER DEFAULT 0,
route TEXT
);`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating feature_status table: %s", err.Error())
}
table_schema = `CREATE TABLE IF NOT EXISTS licenses_v3 (
id TEXT PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
data TEXT
);`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating licenses_v3 table: %s", err.Error())
}
return nil
}

View File

@@ -18,7 +18,6 @@ import (
"go.signoz.io/signoz/pkg/config/fileprovider"
"go.signoz.io/signoz/pkg/query-service/auth"
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
"google.golang.org/grpc"
@@ -141,6 +140,10 @@ func main() {
envprovider.NewFactory(),
fileprovider.NewFactory(),
},
}, signoz.DeprecatedFlags{
MaxIdleConns: maxIdleConns,
MaxOpenConns: maxOpenConns,
DialTimeout: dialTimeout,
})
if err != nil {
zap.L().Fatal("Failed to create config", zap.Error(err))
@@ -161,9 +164,6 @@ func main() {
PrivateHostPort: baseconst.PrivateHostPort,
DisableRules: disableRules,
RuleRepoURL: ruleRepoURL,
MaxIdleConns: maxIdleConns,
MaxOpenConns: maxOpenConns,
DialTimeout: dialTimeout,
CacheConfigPath: cacheConfigPath,
FluxInterval: fluxInterval,
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
@@ -182,12 +182,6 @@ func main() {
zap.L().Info("JWT secret key set successfully.")
}
if err := migrate.Migrate(signoz.SQLStore.SQLxDB()); err != nil {
zap.L().Error("Failed to migrate", zap.Error(err))
} else {
zap.L().Info("Migration successful")
}
server, err := app.NewServer(serverOptions)
if err != nil {
zap.L().Fatal("Failed to create server", zap.Error(err))

View File

@@ -1,7 +1,6 @@
package model
import (
"encoding/base64"
"encoding/json"
"fmt"
"reflect"
@@ -61,37 +60,6 @@ type LicensePlan struct {
Status string `json:"status"`
}
func (l *License) ParsePlan() error {
l.LicensePlan = LicensePlan{}
planData, err := base64.StdEncoding.DecodeString(l.PlanDetails)
if err != nil {
return err
}
plan := LicensePlan{}
err = json.Unmarshal([]byte(planData), &plan)
if err != nil {
l.ValidationMessage = "failed to parse plan from license"
return errors.Wrap(err, "failed to parse plan from license")
}
l.LicensePlan = plan
l.ParseFeatures()
return nil
}
func (l *License) ParseFeatures() {
switch l.PlanKey {
case Pro:
l.FeatureSet = ProPlan
case Enterprise:
l.FeatureSet = EnterprisePlan
default:
l.FeatureSet = BasicPlan
}
}
type Licenses struct {
TrialStart int64 `json:"trialStart"`
TrialEnd int64 `json:"trialEnd"`
@@ -171,8 +139,8 @@ func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
if err != nil {
return nil, err
}
// if license status is inactive then default it to basic
if status == LicenseStatusInactive {
// if license status is invalid then default it to basic
if status == LicenseStatusInvalid {
planName = PlanNameBasic
}

View File

@@ -21,7 +21,7 @@ var (
)
var (
LicenseStatusInactive = "INACTIVE"
LicenseStatusInvalid = "INVALID"
)
const DisableUpsell = "DISABLE_UPSELL"
@@ -157,6 +157,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -279,6 +286,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -415,4 +429,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
"sync/atomic"
@@ -46,9 +45,9 @@ type Manager struct {
tenantID string
}
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn, chUrl string) (*Manager, error) {
hostNameRegex := regexp.MustCompile(`tcp://(?P<hostname>.*):`)
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(os.Getenv("ClickHouseUrl"))
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(chUrl)
tenantID := ""
if len(hostNameRegexMatches) == 2 {

View File

@@ -107,6 +107,7 @@
"react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
@@ -178,6 +179,7 @@
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-helmet-async": "1.0.3",
"@types/react-lottie": "1.2.10",
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
@@ -218,6 +220,7 @@
"portfinder-sync": "^0.0.2",
"postcss": "8.4.38",
"prettier": "2.2.1",
"prop-types": "15.8.1",
"raw-loader": "4.0.2",
"react-hooks-testing-library": "0.6.0",
"react-hot-loader": "^4.13.0",

View File

@@ -0,0 +1,6 @@
<svg width="57" height="57" viewBox="0 0 57 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="solid-check-circle-2">
<path id="Vector" d="M28.2498 51.9638C41.1368 51.9638 51.5832 41.5175 51.5832 28.6305C51.5832 15.7435 41.1368 5.29712 28.2498 5.29712C15.3628 5.29712 4.9165 15.7435 4.9165 28.6305C4.9165 41.5175 15.3628 51.9638 28.2498 51.9638Z" fill="#4E74F8" stroke="#4E74F8" stroke-width="4.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M16.25 27.6304L24.2115 36.6304L39.25 22.6304" stroke="#121317" stroke-width="4.66667" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@@ -0,0 +1,23 @@
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="amazon_web_services_logo.svg" clip-path="url(#clip0_2552_26387)">
<g id="Group">
<path id="Vector" d="M6.82058 6.05151C6.82058 6.34672 6.85997 6.58289 6.89935 6.76001C6.95843 6.93714 7.03721 7.11426 7.15537 7.33075C7.19476 7.38979 7.21445 7.44883 7.21445 7.50788C7.21445 7.5866 7.17506 7.66532 7.0569 7.74404L6.56456 8.07861C6.48579 8.11797 6.42671 8.15733 6.36762 8.15733C6.28885 8.15733 6.21008 8.11797 6.1313 8.03925C6.01314 7.92117 5.93437 7.80308 5.85559 7.66532C5.77682 7.52756 5.69804 7.38979 5.61927 7.19299C5.00876 7.92117 4.24071 8.27542 3.29542 8.27542C2.62584 8.27542 2.1138 8.07861 1.71993 7.70468C1.32606 7.33075 1.12912 6.81906 1.12912 6.18928C1.12912 5.52014 1.36544 4.96908 1.83809 4.57547C2.31074 4.18186 2.96063 3.96538 3.76807 3.96538C4.04378 3.96538 4.31949 3.98506 4.5952 4.02442C4.8906 4.06378 5.18601 4.12282 5.50111 4.20154V3.63081C5.50111 3.04039 5.38294 2.60742 5.12693 2.37125C4.87091 2.13508 4.45734 2.017 3.84684 2.017C3.57113 2.017 3.29542 2.05636 3.00001 2.1154C2.70461 2.19413 2.4289 2.27285 2.15319 2.39093C2.03503 2.43029 1.93656 2.46965 1.87748 2.48933C1.8184 2.48933 1.77901 2.50901 1.75932 2.50901C1.64115 2.50901 1.60177 2.43029 1.60177 2.27285V1.87924C1.60177 1.76115 1.62146 1.66275 1.66085 1.60371C1.70024 1.54467 1.77901 1.48563 1.87748 1.44626C2.15319 1.3085 2.48798 1.19042 2.86216 1.09201C3.25603 0.993612 3.6499 0.93457 4.08316 0.93457C5.02846 0.93457 5.69804 1.15106 6.15099 1.56435C6.58425 1.99732 6.80088 2.6271 6.80088 3.51272L6.82058 6.05151ZM3.63021 7.25203C3.88623 7.25203 4.16194 7.21267 4.43765 7.11426C4.71336 7.01586 4.96938 6.83874 5.18601 6.60257C5.30417 6.44513 5.40264 6.28768 5.46172 6.09088C5.50111 5.89407 5.54049 5.67758 5.54049 5.40206V5.06749C5.30417 5.00845 5.06785 4.96908 4.81183 4.92972C4.55581 4.89036 4.31949 4.89036 4.06347 4.89036C3.53174 4.89036 3.15756 4.98876 2.88185 5.20525C2.62584 5.42174 2.48798 5.71695 2.48798 6.11056C2.48798 6.48449 2.58645 6.76001 2.78338 6.93714C2.98032 7.17331 3.25603 7.25203 3.63021 7.25203ZM9.95187 8.11797C9.81401 8.11797 9.71554 8.09829 9.65646 8.03925C9.59738 7.99989 9.5383 7.88181 9.49891 7.72436L7.64771 1.62339C7.60832 1.46595 7.56894 1.36754 7.56894 1.3085C7.56894 1.19042 7.62802 1.1117 7.76587 1.1117H8.53392C8.69147 1.1117 8.78994 1.13138 8.84902 1.19042C8.9081 1.22978 8.96718 1.34786 9.00657 1.50531L10.326 6.72065L11.547 1.50531C11.5864 1.34786 11.6258 1.24946 11.7046 1.19042C11.7637 1.15106 11.8818 1.1117 12.0197 1.1117H12.6499C12.8074 1.1117 12.9059 1.13138 12.965 1.19042C13.0241 1.22978 13.0832 1.34786 13.1225 1.50531L14.3632 6.7797L15.7221 1.50531C15.7615 1.34786 15.8206 1.24946 15.8796 1.19042C15.9387 1.15106 16.0372 1.1117 16.1947 1.1117H16.9234C17.0416 1.1117 17.1203 1.17074 17.1203 1.3085C17.1203 1.34786 17.1203 1.38722 17.1007 1.42658C17.1007 1.46595 17.081 1.54467 17.0416 1.62339L15.1313 7.72436C15.0919 7.88181 15.0328 7.98021 14.9737 8.03925C14.9147 8.07861 14.8162 8.11797 14.6783 8.11797H14.0088C13.8512 8.11797 13.7527 8.09829 13.6937 8.03925C13.6346 7.98021 13.5755 7.88181 13.5361 7.72436L12.3151 2.64678L11.0941 7.72436C11.0547 7.88181 11.0153 7.98021 10.9365 8.03925C10.8775 8.09829 10.7593 8.11797 10.6214 8.11797H9.95187ZM20.0941 8.31478C19.6805 8.31478 19.267 8.27542 18.8731 8.17702C18.4792 8.07861 18.1641 7.98021 17.9672 7.86213C17.849 7.7834 17.7505 7.70468 17.7308 7.64564C17.7112 7.5866 17.6718 7.4882 17.6718 7.42915V7.03554C17.6718 6.8781 17.7308 6.79938 17.849 6.79938C17.8884 6.79938 17.9475 6.79938 17.9869 6.81906C18.0263 6.83874 18.105 6.85842 18.1838 6.89778C18.4595 7.01586 18.7352 7.11426 19.0503 7.17331C19.3654 7.23235 19.6805 7.27171 19.9956 7.27171C20.488 7.27171 20.8818 7.19299 21.1378 7.01586C21.4136 6.83874 21.5514 6.58289 21.5514 6.268C21.5514 6.05151 21.4726 5.87439 21.3348 5.71695C21.1969 5.5595 20.9212 5.44142 20.547 5.30365L19.4048 4.9494C18.8337 4.77228 18.4004 4.49675 18.1444 4.1425C17.8884 3.78825 17.7505 3.41432 17.7505 3.00103C17.7505 2.66646 17.8293 2.37125 17.9672 2.13508C18.105 1.89892 18.302 1.66275 18.5383 1.48563C18.7746 1.3085 19.0503 1.17074 19.3654 1.07233C19.6805 0.973931 20.0153 0.93457 20.3501 0.93457C20.5273 0.93457 20.7046 0.93457 20.8818 0.973931C21.0591 0.993612 21.2363 1.03297 21.3939 1.05265C21.5514 1.09201 21.709 1.13138 21.8468 1.17074C21.9847 1.2101 22.1028 1.26914 22.1816 1.3085C22.2998 1.36754 22.3785 1.42658 22.4179 1.50531C22.4573 1.56435 22.4967 1.66275 22.4967 1.76115V2.13508C22.4967 2.29253 22.4376 2.39093 22.3195 2.39093C22.2604 2.39093 22.1619 2.35157 22.0241 2.29253C21.5711 2.09572 21.0788 1.97764 20.5077 1.97764C20.0547 1.97764 19.7002 2.05636 19.4639 2.19413C19.2276 2.33189 19.0897 2.56806 19.0897 2.90263C19.0897 3.11911 19.1685 3.31592 19.326 3.45368C19.4836 3.61113 19.779 3.74889 20.1926 3.88665L21.3151 4.2409C21.8862 4.41803 22.2998 4.67388 22.5361 4.98876C22.7724 5.30365 22.8906 5.67758 22.8906 6.09088C22.8906 6.42545 22.8118 6.74033 22.6936 6.99618C22.5558 7.27171 22.3589 7.50788 22.1225 7.685C21.8862 7.88181 21.5908 8.01957 21.256 8.11797C20.8621 8.27542 20.488 8.31478 20.0941 8.31478Z" fill="white"/>
<g id="Group_2">
<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M21.5908 12.1525C18.9912 14.0615 15.2101 15.0849 11.9803 15.0849C7.43108 15.0849 3.33481 13.4121 0.242906 10.6174C0.00658259 10.4009 0.223213 10.1057 0.518617 10.2632C3.86653 12.2115 7.9825 13.3727 12.256 13.3727C15.1313 13.3727 18.302 12.7823 21.2166 11.5424C21.6302 11.3653 22.0044 11.8376 21.5908 12.1525Z" fill="#FF9900"/>
<path id="Vector_3" fill-rule="evenodd" clip-rule="evenodd" d="M22.6543 10.9324C22.3195 10.4994 20.4683 10.7356 19.6214 10.834C19.3654 10.8734 19.326 10.6372 19.5624 10.4798C21.0394 9.43669 23.4814 9.7319 23.7571 10.0861C24.0328 10.4404 23.6783 12.8808 22.2998 14.0419C22.0831 14.2191 21.8862 14.1207 21.9847 13.8845C22.2998 13.0973 22.989 11.3457 22.6543 10.9324Z" fill="#FF9900"/>
</g>
</g>
<g id="Group_3">
<path id="Vector_4" d="M6.82058 6.05151C6.82058 6.34672 6.85997 6.58289 6.89935 6.76001C6.95843 6.93714 7.03721 7.11426 7.15537 7.33075C7.19476 7.38979 7.21445 7.44883 7.21445 7.50788C7.21445 7.5866 7.17506 7.66532 7.0569 7.74404L6.56456 8.07861C6.48579 8.11797 6.42671 8.15733 6.36762 8.15733C6.28885 8.15733 6.21008 8.11797 6.1313 8.03925C6.01314 7.92117 5.93437 7.80308 5.85559 7.66532C5.77682 7.52756 5.69804 7.38979 5.61927 7.19299C5.00876 7.92117 4.24071 8.27542 3.29542 8.27542C2.62584 8.27542 2.1138 8.07861 1.71993 7.70468C1.32606 7.33075 1.12912 6.81906 1.12912 6.18928C1.12912 5.52014 1.36544 4.96908 1.83809 4.57547C2.31074 4.18186 2.96063 3.96538 3.76807 3.96538C4.04378 3.96538 4.31949 3.98506 4.5952 4.02442C4.8906 4.06378 5.18601 4.12282 5.50111 4.20154V3.63081C5.50111 3.04039 5.38294 2.60742 5.12693 2.37125C4.87091 2.13508 4.45734 2.017 3.84684 2.017C3.57113 2.017 3.29542 2.05636 3.00001 2.1154C2.70461 2.19413 2.4289 2.27285 2.15319 2.39093C2.03503 2.43029 1.93656 2.46965 1.87748 2.48933C1.8184 2.48933 1.77901 2.50901 1.75932 2.50901C1.64115 2.50901 1.60177 2.43029 1.60177 2.27285V1.87924C1.60177 1.76115 1.62146 1.66275 1.66085 1.60371C1.70024 1.54467 1.77901 1.48563 1.87748 1.44626C2.15319 1.3085 2.48798 1.19042 2.86216 1.09201C3.25603 0.993612 3.6499 0.93457 4.08316 0.93457C5.02846 0.93457 5.69804 1.15106 6.15099 1.56435C6.58425 1.99732 6.80088 2.6271 6.80088 3.51272L6.82058 6.05151ZM3.63021 7.25203C3.88623 7.25203 4.16194 7.21267 4.43765 7.11426C4.71336 7.01586 4.96938 6.83874 5.18601 6.60257C5.30417 6.44513 5.40264 6.28768 5.46172 6.09088C5.50111 5.89407 5.54049 5.67758 5.54049 5.40206V5.06749C5.30417 5.00845 5.06785 4.96908 4.81183 4.92972C4.55581 4.89036 4.31949 4.89036 4.06347 4.89036C3.53174 4.89036 3.15756 4.98876 2.88185 5.20525C2.62584 5.42174 2.48798 5.71695 2.48798 6.11056C2.48798 6.48449 2.58645 6.76001 2.78338 6.93714C2.98032 7.17331 3.25603 7.25203 3.63021 7.25203ZM9.95187 8.11797C9.81401 8.11797 9.71554 8.09829 9.65646 8.03925C9.59738 7.99989 9.5383 7.88181 9.49891 7.72436L7.64771 1.62339C7.60832 1.46595 7.56894 1.36754 7.56894 1.3085C7.56894 1.19042 7.62802 1.1117 7.76587 1.1117H8.53392C8.69147 1.1117 8.78994 1.13138 8.84902 1.19042C8.9081 1.22978 8.96718 1.34786 9.00657 1.50531L10.326 6.72065L11.547 1.50531C11.5864 1.34786 11.6258 1.24946 11.7046 1.19042C11.7637 1.15106 11.8818 1.1117 12.0197 1.1117H12.6499C12.8074 1.1117 12.9059 1.13138 12.965 1.19042C13.0241 1.22978 13.0832 1.34786 13.1225 1.50531L14.3632 6.7797L15.7221 1.50531C15.7615 1.34786 15.8206 1.24946 15.8796 1.19042C15.9387 1.15106 16.0372 1.1117 16.1947 1.1117H16.9234C17.0416 1.1117 17.1203 1.17074 17.1203 1.3085C17.1203 1.34786 17.1203 1.38722 17.1007 1.42658C17.1007 1.46595 17.081 1.54467 17.0416 1.62339L15.1313 7.72436C15.0919 7.88181 15.0328 7.98021 14.9737 8.03925C14.9147 8.07861 14.8162 8.11797 14.6783 8.11797H14.0088C13.8512 8.11797 13.7527 8.09829 13.6937 8.03925C13.6346 7.98021 13.5755 7.88181 13.5361 7.72436L12.3151 2.64678L11.0941 7.72436C11.0547 7.88181 11.0153 7.98021 10.9365 8.03925C10.8775 8.09829 10.7593 8.11797 10.6214 8.11797H9.95187ZM20.0941 8.31478C19.6805 8.31478 19.267 8.27542 18.8731 8.17702C18.4792 8.07861 18.1641 7.98021 17.9672 7.86213C17.849 7.7834 17.7505 7.70468 17.7308 7.64564C17.7112 7.5866 17.6718 7.4882 17.6718 7.42915V7.03554C17.6718 6.8781 17.7308 6.79938 17.849 6.79938C17.8884 6.79938 17.9475 6.79938 17.9869 6.81906C18.0263 6.83874 18.105 6.85842 18.1838 6.89778C18.4595 7.01586 18.7352 7.11426 19.0503 7.17331C19.3654 7.23235 19.6805 7.27171 19.9956 7.27171C20.488 7.27171 20.8818 7.19299 21.1378 7.01586C21.4136 6.83874 21.5514 6.58289 21.5514 6.268C21.5514 6.05151 21.4726 5.87439 21.3348 5.71695C21.1969 5.5595 20.9212 5.44142 20.547 5.30365L19.4048 4.9494C18.8337 4.77228 18.4004 4.49675 18.1444 4.1425C17.8884 3.78825 17.7505 3.41432 17.7505 3.00103C17.7505 2.66646 17.8293 2.37125 17.9672 2.13508C18.105 1.89892 18.302 1.66275 18.5383 1.48563C18.7746 1.3085 19.0503 1.17074 19.3654 1.07233C19.6805 0.973931 20.0153 0.93457 20.3501 0.93457C20.5273 0.93457 20.7046 0.93457 20.8818 0.973931C21.0591 0.993612 21.2363 1.03297 21.3939 1.05265C21.5514 1.09201 21.709 1.13138 21.8468 1.17074C21.9847 1.2101 22.1028 1.26914 22.1816 1.3085C22.2998 1.36754 22.3785 1.42658 22.4179 1.50531C22.4573 1.56435 22.4967 1.66275 22.4967 1.76115V2.13508C22.4967 2.29253 22.4376 2.39093 22.3195 2.39093C22.2604 2.39093 22.1619 2.35157 22.0241 2.29253C21.5711 2.09572 21.0788 1.97764 20.5077 1.97764C20.0547 1.97764 19.7002 2.05636 19.4639 2.19413C19.2276 2.33189 19.0897 2.56806 19.0897 2.90263C19.0897 3.11911 19.1685 3.31592 19.326 3.45368C19.4836 3.61113 19.779 3.74889 20.1926 3.88665L21.3151 4.2409C21.8862 4.41803 22.2998 4.67388 22.5361 4.98876C22.7724 5.30365 22.8906 5.67758 22.8906 6.09088C22.8906 6.42545 22.8118 6.74033 22.6936 6.99618C22.5558 7.27171 22.3589 7.50788 22.1225 7.685C21.8862 7.88181 21.5908 8.01957 21.256 8.11797C20.8621 8.27542 20.488 8.31478 20.0941 8.31478Z" fill="white"/>
<g id="Group_4">
<path id="Vector_5" fill-rule="evenodd" clip-rule="evenodd" d="M21.5908 12.1525C18.9912 14.0615 15.2101 15.0849 11.9803 15.0849C7.43108 15.0849 3.33481 13.4121 0.242906 10.6174C0.00658259 10.4009 0.223213 10.1057 0.518617 10.2632C3.86653 12.2115 7.9825 13.3727 12.256 13.3727C15.1313 13.3727 18.302 12.7823 21.2166 11.5424C21.6302 11.3653 22.0044 11.8376 21.5908 12.1525Z" fill="#FF9900"/>
<path id="Vector_6" fill-rule="evenodd" clip-rule="evenodd" d="M22.6543 10.9324C22.3195 10.4994 20.4683 10.7356 19.6214 10.834C19.3654 10.8734 19.326 10.6372 19.5624 10.4798C21.0394 9.43669 23.4814 9.7319 23.7571 10.0861C24.0328 10.4404 23.6783 12.8808 22.2998 14.0419C22.0831 14.2191 21.8862 14.1207 21.9847 13.8845C22.2998 13.0973 22.989 11.3457 22.6543 10.9324Z" fill="#FF9900"/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_2552_26387">
<rect width="23.7111" height="14.17" fill="white" transform="translate(0.14444 0.915039)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<path fill="#252F3E" d="M4.51 7.687c0 .197.02.357.058.475.042.117.096.245.17.384a.233.233 0 01.037.123c0 .053-.032.107-.1.16l-.336.224a.255.255 0 01-.138.048c-.054 0-.107-.026-.16-.074a1.652 1.652 0 01-.192-.251 4.137 4.137 0 01-.165-.315c-.415.491-.936.737-1.564.737-.447 0-.804-.129-1.064-.385-.261-.256-.394-.598-.394-1.025 0-.454.16-.822.484-1.1.325-.278.756-.416 1.304-.416.18 0 .367.016.564.042.197.027.4.07.612.118v-.39c0-.406-.085-.689-.25-.854-.17-.166-.458-.246-.868-.246-.186 0-.377.022-.574.07a4.23 4.23 0 00-.575.181 1.525 1.525 0 01-.186.07.326.326 0 01-.085.016c-.075 0-.112-.054-.112-.166v-.262c0-.085.01-.15.037-.186a.399.399 0 01.15-.113c.185-.096.409-.176.67-.24.26-.07.537-.101.83-.101.633 0 1.096.144 1.394.432.293.288.442.726.442 1.314v1.73h.01zm-2.161.811c.175 0 .356-.032.548-.096.191-.064.362-.182.505-.342a.848.848 0 00.181-.341c.032-.129.054-.283.054-.465V7.03a4.43 4.43 0 00-.49-.09 3.996 3.996 0 00-.5-.033c-.357 0-.618.07-.793.214-.176.144-.26.347-.26.614 0 .25.063.437.196.566.128.133.314.197.559.197zm4.273.577c-.096 0-.16-.016-.202-.054-.043-.032-.08-.106-.112-.208l-1.25-4.127a.938.938 0 01-.049-.214c0-.085.043-.133.128-.133h.522c.1 0 .17.016.207.053.043.032.075.107.107.208l.894 3.535.83-3.535c.026-.106.058-.176.1-.208a.365.365 0 01.214-.053h.425c.102 0 .17.016.213.053.043.032.08.107.101.208l.841 3.578.92-3.578a.458.458 0 01.107-.208.346.346 0 01.208-.053h.495c.085 0 .133.043.133.133 0 .027-.006.054-.01.086a.76.76 0 01-.038.133l-1.283 4.127c-.032.107-.069.177-.111.209a.34.34 0 01-.203.053h-.457c-.101 0-.17-.016-.213-.053-.043-.038-.08-.107-.101-.214L8.213 5.37l-.82 3.439c-.026.107-.058.176-.1.213-.043.038-.118.054-.213.054h-.458zm6.838.144a3.51 3.51 0 01-.82-.096c-.266-.064-.473-.134-.612-.214-.085-.048-.143-.101-.165-.15a.378.378 0 01-.031-.149v-.272c0-.112.042-.166.122-.166a.3.3 0 01.096.016c.032.011.08.032.133.054.18.08.378.144.585.187.213.042.42.064.633.064.336 0 .596-.059.777-.176a.575.575 0 00.277-.508.52.52 0 00-.144-.373c-.095-.102-.276-.193-.537-.278l-.772-.24c-.388-.123-.676-.305-.851-.545a1.275 1.275 0 01-.266-.774c0-.224.048-.422.143-.593.096-.17.224-.32.384-.438.16-.122.34-.213.553-.277.213-.064.436-.091.67-.091.118 0 .24.005.357.021.122.016.234.038.346.06.106.026.208.052.303.085.096.032.17.064.224.096a.46.46 0 01.16.133.289.289 0 01.047.176v.251c0 .112-.042.171-.122.171a.552.552 0 01-.202-.064 2.427 2.427 0 00-1.022-.208c-.303 0-.543.048-.708.15-.165.1-.25.256-.25.475 0 .149.053.277.16.379.106.101.303.202.585.293l.756.24c.383.123.66.294.825.513.165.219.244.47.244.748 0 .23-.047.437-.138.619a1.436 1.436 0 01-.388.47c-.165.133-.362.23-.591.299-.24.075-.49.112-.761.112z"/>
<g fill="#F90" fill-rule="evenodd" clip-rule="evenodd">

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -57,5 +57,8 @@
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer"
}

View File

@@ -156,7 +156,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
activeLicenseV3.state === LicenseState.DEFAULTED;
if (shouldSuspendWorkspace && currentRoute) {
navigateToWorkSpaceSuspended(currentRoute);

View File

@@ -264,3 +264,8 @@ export const CeleryOverview = Loadable(
/* webpackChunkName: "CeleryOverview" */ 'pages/Celery/CeleryOverview/CeleryOverview'
),
);
export const MetricsExplorer = Loadable(
() =>
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
);

View File

@@ -28,7 +28,7 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MQDetailPage,
MetricsExplorer,
MySettings,
NewDashboardPage,
OldLogsExplorer,
@@ -395,10 +395,10 @@ const routes: AppRoutes[] = [
key: 'INTEGRATIONS',
},
{
path: ROUTES.MESSAGING_QUEUES,
path: ROUTES.MESSAGING_QUEUES_KAFKA,
exact: true,
component: MessagingQueues,
key: 'MESSAGING_QUEUES',
key: 'MESSAGING_QUEUES_KAFKA',
isPrivate: true,
},
{
@@ -416,10 +416,10 @@ const routes: AppRoutes[] = [
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_DETAIL,
path: ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL,
exact: true,
component: MQDetailPage,
key: 'MESSAGING_QUEUES_DETAIL',
component: MessagingQueues,
key: 'MESSAGING_QUEUES_KAFKA_DETAIL',
isPrivate: true,
},
{
@@ -436,6 +436,27 @@ const routes: AppRoutes[] = [
key: 'INFRASTRUCTURE_MONITORING_KUBERNETES',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_EXPLORER,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_VIEWS,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {
@@ -461,6 +482,7 @@ export const oldRoutes = [
'/logs-save-views',
'/traces-save-views',
'/settings/access-tokens',
'/messaging-queues',
];
export const oldNewRoutesMapping: Record<string, string> = {
@@ -470,6 +492,7 @@ export const oldNewRoutesMapping: Record<string, string> = {
'/logs-save-views': '/logs/saved-views',
'/traces-save-views': '/traces/saved-views',
'/settings/access-tokens': '/settings/api-keys',
'/messaging-queues': '/messaging-queues/overview',
};
export const ROUTES_NOT_TO_BE_OVERRIDEN: string[] = [

View File

@@ -0,0 +1,19 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
const removeAwsIntegrationAccount = async (
accountId: string,
): Promise<SuccessResponse<Record<string, never>> | ErrorResponse> => {
const response = await axios.post(
`/cloud-integrations/aws/accounts/${accountId}/disconnect`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default removeAwsIntegrationAccount;

View File

@@ -0,0 +1,88 @@
import axios from 'api';
import {
CloudAccount,
Service,
ServiceData,
UpdateServiceConfigPayload,
UpdateServiceConfigResponse,
} from 'container/CloudIntegrationPage/ServicesSection/types';
import {
AccountConfigPayload,
AccountConfigResponse,
ConnectionParams,
ConnectionUrlResponse,
} from 'types/api/integrations/aws';
export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
const response = await axios.get('/cloud-integrations/aws/accounts');
return response.data.data.accounts;
};
export const getAwsServices = async (
cloudAccountId?: string,
): Promise<Service[]> => {
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get('/cloud-integrations/aws/services', {
params,
});
return response.data.data.services;
};
export const getServiceDetails = async (
serviceId: string,
cloudAccountId?: string,
): Promise<ServiceData> => {
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get(
`/cloud-integrations/aws/services/${serviceId}`,
{ params },
);
return response.data.data;
};
export const generateConnectionUrl = async (params: {
agent_config: { region: string };
account_config: { regions: string[] };
account_id?: string;
}): Promise<ConnectionUrlResponse> => {
const response = await axios.post(
'/cloud-integrations/aws/accounts/generate-connection-url',
params,
);
return response.data.data;
};
export const updateAccountConfig = async (
accountId: string,
payload: AccountConfigPayload,
): Promise<AccountConfigResponse> => {
const response = await axios.post<AccountConfigResponse>(
`/cloud-integrations/aws/accounts/${accountId}/config`,
payload,
);
return response.data;
};
export const updateServiceConfig = async (
serviceId: string,
payload: UpdateServiceConfigPayload,
): Promise<UpdateServiceConfigResponse> => {
const response = await axios.post<UpdateServiceConfigResponse>(
`/cloud-integrations/aws/services/${serviceId}/config`,
payload,
);
return response.data;
};
export const getConnectionParams = async (): Promise<ConnectionParams> => {
const response = await axios.get(
'/cloud-integrations/aws/accounts/generate-connection-params',
);
return response.data.data;
};

View File

@@ -1,4 +1,4 @@
import axios from 'api';
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';

View File

@@ -1,4 +1,4 @@
import { ApiV2Instance as axios } from 'api';
import { ApiV3Instance as axios } from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/licenses/getAll';

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
import './CeleryOverviewConfigOptions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Row, Select, Spin, Tooltip } from 'antd';
import { Row, Select, Spin } from 'antd';
import {
getValuesFromQueryParams,
setQueryParamsFromOptions,
@@ -10,10 +9,7 @@ import { useCeleryFilterOptions } from 'components/CeleryTask/useCeleryFilterOpt
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, Share2 } from 'lucide-react';
import { useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
interface SelectOptionConfig {
placeholder: string;
@@ -66,10 +62,6 @@ function FilterSelect({
}
function CeleryOverviewConfigOptions(): JSX.Element {
const [isURLCopied, setIsURLCopied] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
const selectConfigs: SelectOptionConfig[] = [
{
placeholder: 'Service Name',
@@ -98,14 +90,6 @@ function CeleryOverviewConfigOptions(): JSX.Element {
},
];
const handleShareURL = (): void => {
handleCopyToClipboard(window.location.href);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
};
return (
<div className="celery-overview-filters">
<Row className="celery-filters">
@@ -118,19 +102,6 @@ function CeleryOverviewConfigOptions(): JSX.Element {
/>
))}
</Row>
<Tooltip title="Share this" arrow={false}>
<Button
className="periscope-btn copy-url-btn"
onClick={handleShareURL}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Share2 size={14} />
)
}
/>
</Tooltip>
</div>
);
}

View File

@@ -12,93 +12,137 @@
cursor: pointer;
}
.celery-overview-table {
.ant-table {
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
.celery-overview-table-container {
width: 100%;
background: var(--bg-ink-500);
border-bottom: none;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;
}
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
background: var(--bg-ink-500);
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.column-header-left {
text-align: left;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
> .ant-input-search {
margin-bottom: 8px;
}
.ant-pagination {
position: relative;
bottom: 0;
width: 100%;
background: var(--bg-ink-500);
padding: 4px;
margin: 0;
.celery-overview-table {
.ant-table {
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
// this is to offset intercom icon
padding-right: 72px;
background: var(--bg-ink-500);
border-bottom: none;
.ant-pagination-item {
border-radius: 4px;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
&-active {
background: var(--bg-robin-500);
border-color: var(--bg-robin-500);
&::before {
background-color: transparent;
}
}
a {
color: var(--bg-ink-500) !important;
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
background: var(--bg-ink-500);
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.column-header-left {
text-align: left;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
}
.ant-pagination {
position: relative;
bottom: 0;
width: 100%;
background: var(--bg-ink-500);
padding: 4px;
margin: 0;
// this is to offset intercom icon
padding-right: 72px;
.ant-pagination-item {
border-radius: 4px;
&-active {
background: var(--bg-robin-500);
border-color: var(--bg-robin-500);
a {
color: var(--bg-ink-500) !important;
}
}
}
}
}
}
.lightMode {
.celery-overview-table-container {
.celery-overview-table {
.ant-table {
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.ant-table-cell {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
.ant-pagination {
background: var(--bg-vanilla-100);
.ant-pagination-item {
&-active {
background: var(--bg-robin-500);
border-color: var(--bg-robin-500);
a {
color: var(--bg-vanilla-100) !important;
}
}
}
}
}

View File

@@ -218,35 +218,44 @@ function getColumns(data: RowData[]): TableColumnsType<RowData> {
showTitle: false,
},
width: 200,
sorter: (a: RowData, b: RowData): number =>
String(a.error_percentage).localeCompare(String(b.error_percentage)),
sorter: (a: RowData, b: RowData): number => {
const aValue = Number(a.error_percentage);
const bValue = Number(b.error_percentage);
return aValue - bValue;
},
render: ProgressRender,
},
{
title: 'LATENCY (P95)',
title: 'LATENCY (P95) in ms',
dataIndex: 'p95_latency',
key: 'p95_latency',
ellipsis: {
showTitle: false,
},
width: 100,
sorter: (a: RowData, b: RowData): number =>
String(a.p95_latency).localeCompare(String(b.p95_latency)),
sorter: (a: RowData, b: RowData): number => {
const aValue = Number(a.p95_latency);
const bValue = Number(b.p95_latency);
return aValue - bValue;
},
render: (value: number | string): string => {
if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
},
},
{
title: 'THROUGHPUT',
title: 'THROUGHPUT (ops/s)',
dataIndex: 'throughput',
key: 'throughput',
ellipsis: {
showTitle: false,
},
width: 100,
sorter: (a: RowData, b: RowData): number =>
String(a.throughput).localeCompare(String(b.throughput)),
sorter: (a: RowData, b: RowData): number => {
const aValue = Number(a.throughput);
const bValue = Number(b.throughput);
return aValue - bValue;
},
render: (value: number | string): string => {
if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
@@ -400,6 +409,15 @@ export default function CeleryOverviewTable({
confirm();
};
// Add defaultSorting state
const [sortedInfo, setSortedInfo] = useState<{
columnKey: string;
order: 'ascend' | 'descend';
}>({
columnKey: 'error_percentage',
order: 'descend',
});
const columns = useMemo(
() =>
getDraggedColumns<RowData>(
@@ -411,10 +429,15 @@ export default function CeleryOverviewTable({
handleSearch,
item.key?.toString(),
),
// Only set defaultSortOrder for error_percentage, but allow sorting for all columns
...(item.key === 'error_percentage' && {
defaultSortOrder: 'descend',
}),
sortOrder: sortedInfo.columnKey === item.key ? sortedInfo.order : null,
})),
draggedColumns,
),
[tableData, draggedColumns],
[tableData, draggedColumns, sortedInfo],
);
const handleDragColumn = useCallback(
(fromIndex: number, toIndex: number) =>
@@ -459,7 +482,7 @@ export default function CeleryOverviewTable({
]);
return (
<div style={{ width: '100%' }}>
<div className="celery-overview-table-container">
<Input.Search
placeholder="Search across all columns"
onChange={(e): void => setSearchText(e.target.value)}
@@ -488,6 +511,12 @@ export default function CeleryOverviewTable({
className: 'clickable-row',
})}
tableLayout="fixed"
onChange={(_pagination, _filters, sorter): void => {
setSortedInfo({
columnKey: (sorter as { columnKey: string }).columnKey,
order: (sorter as { order: 'ascend' | 'descend' }).order,
});
}}
/>
</div>
);

View File

@@ -1,14 +1,10 @@
import './CeleryTaskConfigOptions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Spin, Tooltip, Typography } from 'antd';
import { Select, Spin, Typography } from 'antd';
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, Share2 } from 'lucide-react';
import { useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import {
getValuesFromQueryParams,
@@ -23,11 +19,8 @@ function CeleryTaskConfigOptions(): JSX.Element {
const history = useHistory();
const location = useLocation();
const [isURLCopied, setIsURLCopied] = useState(false);
const urlQuery = useUrlQuery();
const [, handleCopyToClipboard] = useCopyToClipboard();
return (
<div className="celery-task-filters">
<div className="celery-filters">
@@ -66,25 +59,6 @@ function CeleryTaskConfigOptions(): JSX.Element {
}}
/>
</div>
<Tooltip title="Share this" arrow={false}>
<Button
className="periscope-btn copy-url-btn"
onClick={(): void => {
handleCopyToClipboard(window.location.href);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
}}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Share2 size={14} />
)
}
/>
</Tooltip>
</div>
);
}

View File

@@ -1,13 +1,22 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { DefaultOptionType } from 'antd/es/select';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
export interface Filters {
searchText: string;
attributeKey: string | string[];
aggregateOperator?: string;
dataSource?: DataSource;
aggregateAttribute?: string;
filterAttributeKeyDataType?: DataTypes;
tagType?: string;
}
export interface GetAllFiltersResponse {
@@ -16,23 +25,42 @@ export interface GetAllFiltersResponse {
}
export function useGetAllFilters(props: Filters): GetAllFiltersResponse {
const { searchText, attributeKey } = props;
const {
searchText,
attributeKey,
aggregateOperator,
dataSource,
aggregateAttribute,
filterAttributeKeyDataType,
tagType,
} = props;
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { data, isLoading } = useQuery(
['attributesValues', attributeKey, searchText],
[
REACT_QUERY_KEY.GET_ATTRIBUTE_VALUES,
attributeKey,
searchText,
minTime,
maxTime,
],
async () => {
const keys = Array.isArray(attributeKey) ? attributeKey : [attributeKey];
const responses = await Promise.all(
keys.map((key) =>
getAttributesValues({
aggregateOperator: 'noop',
dataSource: DataSource.TRACES,
aggregateAttribute: '',
aggregateOperator: aggregateOperator || 'noop',
dataSource: dataSource || DataSource.TRACES,
aggregateAttribute: aggregateAttribute || '',
attributeKey: key,
searchText: searchText ?? '',
filterAttributeKeyDataType: DataTypes.String,
tagType: 'tag',
filterAttributeKeyDataType:
filterAttributeKeyDataType || DataTypes.String,
tagType: tagType || 'tag',
}),
),
);

View File

@@ -72,3 +72,51 @@
}
}
}
.lightMode {
.celery-task-detail-drawer {
.ant-drawer-wrapper-body {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
}
.ant-drawer-body {
.ant-card {
.ant-card-body {
background: var(--bg-vanilla-100);
.ant-table {
background: var(--bg-vanilla-100);
}
}
}
}
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-drawer-header-title {
button > svg {
color: var(--bg-ink-500);
}
.ant-drawer-title {
.title {
color: var(--bg-ink-400);
}
.subtitle {
color: var(--bg-ink-300);
}
}
}
}
.ant-drawer-footer {
border-top: 1px solid var(--bg-vanilla-300);
.footer-text {
color: var(--bg-ink-400);
}
}
}
}

View File

@@ -2,24 +2,16 @@ import './CeleryTaskDetail.style.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer, Typography } from 'antd';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { useState } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import CeleryTaskGraph from '../CeleryTaskGraph/CeleryTaskGraph';
import { createFiltersFromData } from '../CeleryUtils';
import { useNavigateToTraces } from '../useNavigateToTraces';
export type CeleryTaskData = {
@@ -39,40 +31,6 @@ export type CeleryTaskDetailProps = {
drawerOpen: boolean;
};
const createFiltersFromData = (
data: Record<string, any>,
): Array<{
id: string;
key: {
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
return Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
.map(([key, value]) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}));
};
export default function CeleryTaskDetail({
widgetData,
taskData,
@@ -85,7 +43,7 @@ export default function CeleryTaskDetail({
!!taskData.entity && !!taskData.timeRange[0] && drawerOpen;
const formatTimestamp = (timestamp: number): string =>
dayjs(timestamp * 1000).format('MM-DD-YYYY hh:mm A');
dayjs(timestamp).format('DD-MM-YYYY hh:mm A');
const [totalTask, setTotalTask] = useState(0);
@@ -93,52 +51,9 @@ export default function CeleryTaskDetail({
setTotalTask((graphData?.result?.[0] as any)?.table?.rows.length);
};
// set time range
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startTime = taskData.timeRange[0];
const endTime = taskData.timeRange[1];
const urlQuery = useUrlQuery();
const location = useLocation();
const history = useHistory();
const dispatch = useDispatch();
useEffect(() => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, startTime.toString());
urlQuery.set(QueryParams.endTime, endTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
if (startTime !== endTime) {
dispatch(UpdateTimeInterval('custom', [startTime, endTime]));
}
return (): void => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
if (selectedTime !== 'custom') {
dispatch(UpdateTimeInterval(selectedTime));
urlQuery.set(QueryParams.relativeTime, selectedTime);
} else {
dispatch(UpdateTimeInterval('custom', [minTime / 1e6, maxTime / 1e6]));
urlQuery.set(QueryParams.startTime, Math.floor(minTime / 1e6).toString());
urlQuery.set(QueryParams.endTime, Math.floor(maxTime / 1e6).toString());
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const navigateToTrace = useNavigateToTraces();
return (
@@ -149,10 +64,8 @@ export default function CeleryTaskDetail({
<Typography.Text className="title">{`Details - ${taskData.entity}`}</Typography.Text>
<div>
<Typography.Text className="subtitle">
{`${formatTimestamp(taskData.timeRange[0])} ${
taskData.timeRange[1]
? `- ${formatTimestamp(taskData.timeRange[1])}`
: ''
{`${formatTimestamp(startTime)} ${
endTime ? `- ${formatTimestamp(endTime)}` : ''
}`}
</Typography.Text>
<Divider type="vertical" />
@@ -185,8 +98,10 @@ export default function CeleryTaskDetail({
...rowData,
[taskData.entity]: taskData.value,
});
navigateToTrace(filters);
navigateToTrace(filters, startTime, endTime);
}}
start={startTime}
end={endTime}
/>
</Drawer>
);

View File

@@ -3,14 +3,12 @@ import './CeleryTaskGraph.style.scss';
import { Color } from '@signozhq/design-tokens';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { isEmpty } from 'lodash-es';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -18,11 +16,14 @@ import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { QueryData } from 'types/api/widgets/getQuery';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import { paths } from '../CeleryUtils';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import { useGetGraphCustomSeries } from '../useGetGraphCustomSeries';
import {
celeryAllStateWidgetData,
celeryFailedStateWidgetData,
@@ -75,26 +76,60 @@ function CeleryTaskBar({
const [barState, setBarState] = useState<CeleryTaskState>(CeleryTaskState.All);
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(
QueryParams.taskName,
urlQuery,
'celery.task_name',
),
[urlQuery],
);
const celeryAllStateData = useMemo(
() => celeryAllStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryAllStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryAllStateData),
[selectedFilters, celeryAllStateData],
);
const celeryFailedStateData = useMemo(
() => celeryFailedStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryFailedStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryFailedStateData),
[selectedFilters, celeryFailedStateData],
);
const celeryRetryStateData = useMemo(
() => celeryRetryStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryRetryStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryRetryStateData),
[selectedFilters, celeryRetryStateData],
);
const celerySuccessStateData = useMemo(
() => celerySuccessStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celerySuccessStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celerySuccessStateData),
[selectedFilters, celerySuccessStateData],
);
const onGraphClick = (
widgetData: Widgets,
xValue: number,
@@ -114,58 +149,26 @@ function CeleryTaskBar({
string,
];
onClick?.({
entity,
value,
timeRange: [start, end],
widgetData,
});
if (!isEmpty(entity) || !isEmpty(value)) {
onClick?.({
entity,
value,
timeRange: [start, end],
widgetData,
});
}
};
const getGraphSeries = (color: string, label: string): any => ({
const { getCustomSeries } = useGetGraphCustomSeries({
isDarkMode,
drawStyle: 'bars',
paths,
lineInterpolation: 'spline',
show: true,
label,
fill: `${color}90`,
stroke: color,
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: color,
colorMapping: {
SUCCESS: Color.BG_FOREST_500,
FAILURE: Color.BG_CHERRY_500,
RETRY: Color.BG_AMBER_400,
},
});
const customSeries = (data: QueryData[]): uPlot.Series[] => {
const configurations: uPlot.Series[] = [
{ label: 'Timestamp', stroke: 'purple' },
];
for (let i = 0; i < data.length; i += 1) {
const { metric = {}, queryName = '', legend = '' } = data[i] || {};
const label = getLabelName(metric, queryName || '', legend || '');
let color = generateColor(
label,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
if (label === 'SUCCESS') {
color = Color.BG_FOREST_500;
}
if (label === 'FAILURE') {
color = Color.BG_CHERRY_500;
}
if (label === 'RETRY') {
color = Color.BG_AMBER_400;
}
const series = getGraphSeries(color, label);
configurations.push(series);
}
return configurations;
};
return (
<Card
isDarkMode={isDarkMode}
@@ -176,50 +179,50 @@ function CeleryTaskBar({
<div className="celery-task-graph-grid-content">
{barState === CeleryTaskState.All && (
<GridCard
widget={celeryAllStateData}
widget={celeryAllStateFilteredData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
onClickHandler={(...args): void =>
onGraphClick(celerySlowestTasksTableWidgetData, ...args)
}
customSeries={customSeries}
customSeries={getCustomSeries}
/>
)}
{barState === CeleryTaskState.Failed && (
<GridCard
widget={celeryFailedStateData}
widget={celeryFailedStateFilteredData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
onClickHandler={(...args): void =>
onGraphClick(celeryFailedTasksTableWidgetData, ...args)
}
customSeries={customSeries}
customSeries={getCustomSeries}
/>
)}
{barState === CeleryTaskState.Retry && (
<GridCard
widget={celeryRetryStateData}
widget={celeryRetryStateFilteredData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
onClickHandler={(...args): void =>
onGraphClick(celeryRetryTasksTableWidgetData, ...args)
}
customSeries={customSeries}
customSeries={getCustomSeries}
/>
)}
{barState === CeleryTaskState.Successful && (
<GridCard
widget={celerySuccessStateData}
widget={celerySuccessStateFilteredData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
onClickHandler={(...args): void =>
onGraphClick(celerySuccessTasksTableWidgetData, ...args)
}
customSeries={customSeries}
customSeries={getCustomSeries}
/>
)}
</div>

View File

@@ -6,52 +6,85 @@
padding-bottom: 16px;
margin-bottom: 16px;
.celery-task-graph-grid {
display: grid;
grid-template-columns: 60% 40%;
align-items: flex-start;
gap: 10px;
.celery-task-graph-bar,
.celery-task-graph-task-latency {
height: 380px !important;
width: 100%;
box-sizing: border-box;
.celery-task-graph {
height: 380px !important;
.celery-task-graph-grid-content {
padding: 6px;
width: 100%;
box-sizing: border-box;
height: 100%;
}
.ant-card-body {
height: calc(100% - 18px);
.ant-card-body {
height: calc(100% - 18px);
.widget-graph-container {
&.bar {
height: calc(100% - 105px);
}
.widget-graph-container {
&.bar {
height: calc(100% - 110px);
}
&.graph {
height: calc(100% - 80px);
}
}
}
}
.celery-task-graph-bar,
.celery-task-graph-task-latency {
height: 380px !important;
.celery-task-graph-worker-count {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--bg-slate-500);
background: linear-gradient(
0deg,
rgba(171, 189, 255, 0) 0%,
rgba(171, 189, 255, 0) 100%
),
#0b0c0e;
.ant-card-body {
height: 100%;
width: 100%;
box-sizing: border-box;
}
.celery-task-graph-grid-content {
padding: 6px;
height: 100%;
.worker-count-text-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.celery-task-graph-worker-count-text {
font-size: 2.5vw;
text-align: center;
}
}
.ant-card-body {
height: calc(100% - 18px);
.worker-count-header {
display: flex;
justify-content: start;
align-items: start;
position: absolute;
height: 100%;
width: 100%;
}
}
.widget-graph-container {
&.bar {
height: calc(100% - 105px);
}
.celery-task-graph {
height: 380px !important;
padding: 6px;
width: 100%;
box-sizing: border-box;
&.graph {
height: calc(100% - 80px);
}
.ant-card-body {
height: calc(100% - 18px);
.widget-graph-container {
&.bar {
height: calc(100% - 110px);
}
}
}
@@ -75,6 +108,89 @@
}
}
}
.metric-based-graphs,
.trace-based-graphs {
display: flex;
flex-direction: column;
gap: 10px;
.metric-page-grid {
display: flex;
flex-direction: row;
gap: 10px;
width: 100%;
.celery-task-graph {
height: 280px !important;
}
}
}
.trace-based-graphs {
.trace-based-graphs-header {
display: grid;
grid-template-columns: 50% 50%;
align-items: center;
gap: 24px;
width: 100%;
}
}
.configure-option-Info {
display: grid;
grid-template-columns: max-content 1fr;
gap: 16px;
align-items: center;
border: 1px dashed var(--bg-slate-50);
border-radius: 4px;
padding: 6px 24px 6px 12px;
width: max-content;
.configure-option-Info-text {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
}
}
.row-panel {
border-radius: 4px;
background: rgba(18, 19, 23, 0.4);
padding: 8px;
display: flex;
gap: 6px;
align-items: center;
height: 48px !important;
width: 100%;
.ant-typography {
font-size: 14px;
font-weight: 500;
}
.row-panel-section {
display: flex;
gap: 6px;
align-items: center;
.row-icon {
color: var(--bg-vanilla-400);
cursor: pointer;
}
.section-title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
}
}
}
.celery-task-states {
@@ -126,3 +242,46 @@
background-color: var(--bg-vanilla-100);
}
}
.lightMode {
.celery-task-graph-grid-container {
.celery-task-graph-worker-count {
border: 1px solid var(--bg-vanilla-300);
background: unset;
}
.row-panel .row-panel-section .section-title {
color: var(--bg-ink-400);
}
}
.celery-task-states {
border-bottom: 1px solid var(--bg-vanilla-300);
&__tab {
&:not([data-last-tab='true']) {
border-right: 1px solid var(--bg-vanilla-300);
}
&--selected {
background-color: var(--bg-vanilla-200);
}
}
&__label {
color: var(--bg-ink-500);
}
&__value {
color: var(--bg-slate-100);
}
&__indicator {
background-color: var(--bg-ink-400);
}
}
.configure-option-Info {
border: 1px dashed var(--bg-robin-400);
}
}

View File

@@ -32,6 +32,9 @@ function CeleryTaskGraph({
openTracesButton,
onOpenTraceBtnClick,
applyCeleryTaskFilter,
customErrorMessage,
start,
end,
}: {
widgetData: Widgets;
onClick?: (task: CaptureDataProps) => void;
@@ -42,6 +45,9 @@ function CeleryTaskGraph({
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
applyCeleryTaskFilter?: boolean;
customErrorMessage?: string;
start?: number;
end?: number;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -116,6 +122,9 @@ function CeleryTaskGraph({
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
version={ENTITY_VERSION_V4}
customErrorMessage={customErrorMessage}
start={start}
end={end}
/>
</Card>
);
@@ -129,6 +138,9 @@ CeleryTaskGraph.defaultProps = {
openTracesButton: false,
onOpenTraceBtnClick: undefined,
applyCeleryTaskFilter: false,
customErrorMessage: undefined,
start: undefined,
end: undefined,
};
export default CeleryTaskGraph;

View File

@@ -1,11 +1,18 @@
import './CeleryTaskGraph.style.scss';
import { useMemo } from 'react';
import { Card, Typography } from 'antd';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import { useCeleryFilterOptions } from '../useCeleryFilterOptions';
import CeleryTaskBar from './CeleryTaskBar';
import CeleryTaskGraph from './CeleryTaskGraph';
import {
@@ -13,26 +20,22 @@ import {
celeryErrorByWorkerWidgetData,
celeryLatencyByWorkerWidgetData,
celeryTasksByWorkerWidgetData,
celeryWorkerOnlineWidgetData,
} from './CeleryTaskGraphUtils';
import CeleryTaskLatencyGraph from './CeleryTaskLatencyGraph';
export default function CeleryTaskGraphGrid({
onClick,
queryEnabled,
configureOptionComponent,
}: {
onClick: (task: CaptureDataProps) => void;
queryEnabled: boolean;
configureOptionComponent?: React.ReactNode;
}): JSX.Element {
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const celeryWorkerOnlineData = useMemo(
() => celeryWorkerOnlineWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryActiveTasksData = useMemo(
() => celeryActiveTasksWidgetData(minTime, maxTime),
[minTime, maxTime],
@@ -65,36 +68,132 @@ export default function CeleryTaskGraphGrid({
'Latency by worker',
];
const { options } = useCeleryFilterOptions(
'worker',
'rate',
DataSource.METRICS,
'flower_task_runtime_seconds_sum',
DataTypes.String,
'tag',
);
const isDarkMode = useIsDarkMode();
const [collapsedSections, setCollapsedSections] = useState<{
[key: string]: boolean;
}>({
metricBasedGraphs: false,
traceBasedGraphs: false,
});
const toggleCollapse = (key: string): void => {
setCollapsedSections((prev) => ({
...prev,
[key]: !prev[key],
}));
};
return (
<div className="celery-task-graph-grid-container">
<div className="celery-task-graph-grid">
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
<CeleryTaskGraph
key={celeryWorkerOnlineData.id}
widgetData={celeryWorkerOnlineData}
queryEnabled={queryEnabled}
/>
<div className="metric-based-graphs">
<CardContainer className="row-card" isDarkMode={isDarkMode}>
<div className="row-panel">
<div className="row-panel-section">
<Typography.Text className="section-title">
Flower Metrics
</Typography.Text>
{collapsedSections.metricBasedGraphs ? (
<ChevronDown
size={14}
onClick={(): void => toggleCollapse('metricBasedGraphs')}
className="row-icon"
/>
) : (
<ChevronUp
size={14}
onClick={(): void => toggleCollapse('metricBasedGraphs')}
className="row-icon"
/>
)}
</div>
</div>
</CardContainer>
{!collapsedSections.metricBasedGraphs && (
<div className="metric-page-grid">
<CeleryTaskGraph
key={celeryActiveTasksData.id}
widgetData={celeryActiveTasksData}
queryEnabled={queryEnabled}
customErrorMessage="Enable Flower metrics to view this graph"
/>
<Card className="celery-task-graph-worker-count">
<div className="worker-count-header">
<Typography.Text className="worker-count-header-text">
Worker Online
</Typography.Text>
</div>
<div className="worker-count-text-container">
<Typography.Text className="celery-task-graph-worker-count-text">
{options.filter((option) => option.value).length}
</Typography.Text>
</div>
</Card>
</div>
)}
</div>
<div className="celery-task-graph-grid">
<CeleryTaskLatencyGraph onClick={onClick} queryEnabled={queryEnabled} />
<CeleryTaskGraph
key={celeryActiveTasksData.id}
widgetData={celeryActiveTasksData}
queryEnabled={queryEnabled}
/>
</div>
<div className="celery-task-graph-grid-bottom">
{bottomWidgetData.map((widgetData, index) => (
<CeleryTaskGraph
key={widgetData.id}
widgetData={widgetData}
onClick={onClick}
queryEnabled={queryEnabled}
rightPanelTitle={rightPanelTitle[index]}
applyCeleryTaskFilter
/>
))}
<div className="trace-based-graphs">
<div className="trace-based-graphs-header">
<CardContainer className="row-card" isDarkMode={isDarkMode}>
<div className="row-panel">
<div className="row-panel-section">
<Typography.Text className="section-title">
Span Based Stats
</Typography.Text>
{collapsedSections.traceBasedGraphs ? (
<ChevronDown
size={14}
onClick={(): void => toggleCollapse('traceBasedGraphs')}
className="row-icon"
/>
) : (
<ChevronUp
size={14}
onClick={(): void => toggleCollapse('traceBasedGraphs')}
className="row-icon"
/>
)}
</div>
</div>
</CardContainer>
<div className="configure-option-Info">
{configureOptionComponent}
<Typography.Text className="configure-option-Info-text">
Click on a graph co-ordinate to see more details
</Typography.Text>
</div>
</div>
{!collapsedSections.traceBasedGraphs && (
<>
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
<CeleryTaskLatencyGraph queryEnabled={queryEnabled} />
<div className="celery-task-graph-grid-bottom">
{bottomWidgetData.map((widgetData, index) => (
<CeleryTaskGraph
key={widgetData.id}
widgetData={widgetData}
onClick={onClick}
queryEnabled={queryEnabled}
rightPanelTitle={rightPanelTitle[index]}
applyCeleryTaskFilter
/>
))}
</div>
</>
)}
</div>
</div>
);
}
CeleryTaskGraphGrid.defaultProps = {
configureOptionComponent: null,
};

View File

@@ -42,21 +42,7 @@ export const celeryAllStateWidgetData = (
disabled: false,
expression: 'A',
filters: {
items: [
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
op: '=',
value: 'tasks.tasks.divide',
},
],
items: [],
op: 'AND',
},
functions: [],
@@ -113,7 +99,7 @@ export const celeryRetryStateWidgetData = (
filters: {
items: [
{
id: '6d97eed3',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -179,7 +165,7 @@ export const celeryFailedStateWidgetData = (
filters: {
items: [
{
id: '5983eae2',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -245,7 +231,7 @@ export const celerySuccessStateWidgetData = (
filters: {
items: [
{
id: '000c5a93',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -332,6 +318,7 @@ export const celeryTasksByWorkerWidgetData = (
timeAggregation: 'rate',
},
],
yAxisUnit: 'cps',
}),
);
@@ -345,44 +332,103 @@ export const celeryErrorByWorkerWidgetData = (
description: 'Represents the number of errors by each worker.',
queryData: [
{
dataSource: DataSource.TRACES,
queryName: 'A',
aggregateOperator: 'count_distinct',
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
dataType: 'string',
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: '',
key: 'span_id',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
timeAggregation: 'count_distinct',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [],
items: [
{
id: uuidv4(),
key: {
dataType: DataTypes.bool,
id: 'has_error--bool----true',
isColumn: true,
isJSON: false,
key: 'has_error',
type: '',
},
op: '=',
value: 'true',
},
],
op: 'AND',
},
functions: [],
expression: 'A',
disabled: true,
stepInterval: getStepInterval(startTime, endTime),
having: [],
limit: null,
orderBy: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
id: 'celery.hostname--string--tag--false',
},
],
having: [],
legend: '{{celery.hostname}}',
limit: 10,
orderBy: [],
queryName: 'A',
legend: '',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
{
dataSource: 'traces',
queryName: 'B',
aggregateOperator: 'count_distinct',
aggregateAttribute: {
dataType: 'string',
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
timeAggregation: 'count_distinct',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'B',
disabled: true,
stepInterval: getStepInterval(startTime, endTime),
having: [],
limit: null,
orderBy: [],
groupBy: [
{
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
id: 'celery.hostname--string--tag--false',
},
],
legend: '',
reduceTo: 'avg',
},
{
queryName: 'F1',
expression: '(A/B)*100',
disabled: false,
legend: '{{celery.hostname}}',
} as any,
],
yAxisUnit: 'percent',
}),
);
@@ -392,7 +438,7 @@ export const celeryLatencyByWorkerWidgetData = (
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Latency by Worker',
title: 'Latency by worker',
description: 'Represents the latency of tasks by each worker.',
queryData: [
{
@@ -444,7 +490,7 @@ export const celeryActiveTasksWidgetData = (
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Tasks/ worker (Active tasks)',
title: 'Active Tasks by worker',
description: 'Represents the number of active tasks.',
queryData: [
{
@@ -487,49 +533,7 @@ export const celeryActiveTasksWidgetData = (
timeAggregation: 'latest',
},
],
}),
);
export const celeryWorkerOnlineWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Worker Online',
description: 'Represents the number of workers online.',
panelTypes: PANEL_TYPES.VALUE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'flower_task_runtime_seconds_sum--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'flower_task_runtime_seconds_sum',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
],
yAxisUnit: 'cps',
}),
);
@@ -584,7 +588,7 @@ export const celeryTaskLatencyWidgetData = (
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'p99',
timeAggregation: type || 'p99',
},
],
yAxisUnit: 'ns',
@@ -668,7 +672,7 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
filters: {
items: [
{
id: '9e09c9ed',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -737,7 +741,7 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
filters: {
items: [
{
id: '2330f906',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -804,7 +808,7 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
filters: {
items: [
{
id: 'ec3df7b7',
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -927,33 +931,19 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: '',
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
op: '=',
value: 'tasks.tasks.divide',
},
],
items: [],
op: 'AND',
},
functions: [],
@@ -963,10 +953,10 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
timeAggregation: 'count_distinct',
},
],
}),
@@ -980,14 +970,14 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: '',
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
@@ -1016,10 +1006,10 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
timeAggregation: 'count_distinct',
},
],
}),
@@ -1033,14 +1023,14 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: '',
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
@@ -1069,10 +1059,10 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
timeAggregation: 'count_distinct',
},
],
}),
@@ -1086,13 +1076,13 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
key: '',
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
@@ -1121,10 +1111,10 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
timeAggregation: 'count_distinct',
},
],
}),

View File

@@ -6,8 +6,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { Button } from 'container/MetricsApplication/Tabs/styles';
import { onGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -16,15 +19,13 @@ import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import {
applyCeleryFilterOnWidgetData,
createFiltersFromData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import {
celeryTaskLatencyWidgetData,
celeryTimeSeriesTablesWidgetData,
} from './CeleryTaskGraphUtils';
import { useNavigateToTraces } from '../useNavigateToTraces';
import { celeryTaskLatencyWidgetData } from './CeleryTaskGraphUtils';
interface TabData {
label: string;
@@ -38,10 +39,8 @@ export enum CeleryTaskGraphState {
}
function CeleryTaskLatencyGraph({
onClick,
queryEnabled,
}: {
onClick: (task: CaptureDataProps) => void;
queryEnabled: boolean;
}): JSX.Element {
const history = useHistory();
@@ -106,31 +105,51 @@ function CeleryTaskLatencyGraph({
[celeryTaskLatencyData, selectedFilters],
);
const onGraphClick = (
xValue: number,
_yValue: number,
_mouseX: number,
_mouseY: number,
data?: {
[key: string]: string;
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const [entityData, setEntityData] = useState<{
entity: string;
value: string;
}>();
const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime);
}, []);
const onGraphClick = useCallback(
(type: string): OnClickPluginOpts['onClick'] => (
xValue,
yValue,
mouseX,
mouseY,
data,
): Promise<void> => {
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = firstDataPoint;
setEntityData({
entity,
value,
});
return onGraphClickHandler(handleSetTimeStamp)(
xValue,
yValue,
mouseX,
mouseY,
type,
);
},
): void => {
const { start, end } = getStartAndEndTimesInMilliseconds(xValue);
[handleSetTimeStamp],
);
// Extract entity and value from data
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = (firstDataPoint || ([] as unknown)) as [
string,
string,
];
const navigateToTraces = useNavigateToTraces();
onClick?.({
entity,
value,
timeRange: [start, end],
widgetData: celeryTimeSeriesTablesWidgetData(entity, value, 'Task Latency'),
const goToTraces = useCallback(() => {
const { start, end } = getStartAndEndTimesInMilliseconds(selectedTimeStamp);
const filters = createFiltersFromData({
[entityData?.entity as string]: entityData?.value,
});
};
navigateToTraces(filters, start, end, true);
}, [entityData, navigateToTraces, selectedTimeStamp]);
return (
<Card
@@ -161,32 +180,62 @@ function CeleryTaskLatencyGraph({
</Row>
<div className="celery-task-graph-grid-content">
{graphState === CeleryTaskGraphState.P99 && (
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
<>
<Button
type="default"
size="small"
id="Celery_p99_latency_button"
onClick={goToTraces}
>
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p99_latency')}
isQueryEnabled={queryEnabled}
/>
</>
)}
{graphState === CeleryTaskGraphState.P95 && (
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
<>
<Button
type="default"
size="small"
id="Celery_p95_latency_button"
onClick={goToTraces}
>
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p95_latency')}
isQueryEnabled={queryEnabled}
/>
</>
)}
{graphState === CeleryTaskGraphState.P90 && (
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
<>
<Button
type="default"
size="small"
id="Celery_p90_latency_button"
onClick={goToTraces}
>
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p90_latency')}
isQueryEnabled={queryEnabled}
/>
</>
)}
</div>
</Card>

View File

@@ -2,8 +2,14 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Dispatch, SetStateAction, useMemo } from 'react';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import {
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
@@ -42,16 +48,29 @@ function CeleryTaskStateGraphConfig({
setBarState(key as CeleryTaskState);
};
const { values, isLoading, isError } = useGetValueFromWidget(
[
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
celeryRetryStateCountWidgetData,
celerySuccessStateCountWidgetData,
],
['celery-task-states'],
const urlQuery = useUrlQuery();
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(
QueryParams.taskName,
urlQuery,
'celery.task_name',
),
[urlQuery],
);
const widgetData = [
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
celeryRetryStateCountWidgetData,
celerySuccessStateCountWidgetData,
].map((data) => applyCeleryFilterOnWidgetData(selectedFilters || [], data));
const { values, isLoading, isError } = useGetValueFromWidget(widgetData, [
'celery-task-states',
]);
return (
<Row className="celery-task-states">
{tabs.map((tab, index) => (
@@ -66,7 +85,13 @@ function CeleryTaskStateGraphConfig({
<div className="celery-task-states__label-wrapper">
<div className="celery-task-states__label">{tab.label}</div>
<div className="celery-task-states__value">
{isLoading ? '-' : isError ? '-' : values[index]}
{isLoading
? '-'
: isError
? '-'
: Number.isNaN(values[index])
? '-'
: Math.round(Number(values[index]))}
</div>
</div>
{tab.key === barState && <div className="celery-task-states__indicator" />}

View File

@@ -90,3 +90,40 @@ export const paths = (
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
};
export const createFiltersFromData = (
data: Record<string, any>,
): Array<{
id: string;
key: {
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
return (
Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
// eslint-disable-next-line sonarjs/no-identical-functions
.map(([key, value]) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}))
);
};

View File

@@ -1,11 +1,18 @@
import { DefaultOptionType } from 'antd/es/select';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useState } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { useGetAllFilters } from './CeleryTaskConfigOptions/useGetCeleryFilters';
export const useCeleryFilterOptions = (
type: string | string[],
aggregateOperator?: string,
dataSource?: DataSource,
aggregateAttribute?: string,
filterAttributeKeyDataType?: DataTypes,
tagType?: string,
): {
searchText: string;
handleSearch: (value: string) => void;
@@ -16,6 +23,11 @@ export const useCeleryFilterOptions = (
const { isFetching, options } = useGetAllFilters({
attributeKey: type,
searchText,
aggregateOperator,
dataSource,
aggregateAttribute,
filterAttributeKeyDataType,
tagType,
});
const handleDebouncedSearch = useDebouncedFn((searchText): void => {
setSearchText(searchText as string);

View File

@@ -0,0 +1,70 @@
import { Color } from '@signozhq/design-tokens';
import { themeColors } from 'constants/theme';
import getLabelName from 'lib/getLabelName';
import { drawStyles } from 'lib/uPlotLib/utils/constants';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { QueryData } from 'types/api/widgets/getQuery';
import { paths } from './CeleryUtils';
interface UseGetGraphCustomSeriesProps {
isDarkMode: boolean;
drawStyle?: typeof drawStyles[keyof typeof drawStyles];
colorMapping?: Record<string, string>;
}
const defaultColorMapping = {
SUCCESS: Color.BG_FOREST_500,
FAILURE: Color.BG_CHERRY_500,
RETRY: Color.BG_AMBER_400,
};
export const useGetGraphCustomSeries = ({
isDarkMode,
drawStyle = 'bars',
colorMapping = defaultColorMapping,
}: UseGetGraphCustomSeriesProps): {
getCustomSeries: (data: QueryData[]) => uPlot.Series[];
} => {
const getGraphSeries = (color: string, label: string): any => ({
drawStyle,
paths,
lineInterpolation: 'spline',
show: true,
label,
fill: `${color}90`,
stroke: color,
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: color,
},
});
const getCustomSeries = (data: QueryData[]): uPlot.Series[] => {
const configurations: uPlot.Series[] = [
{ label: 'Timestamp', stroke: 'purple' },
];
for (let i = 0; i < data.length; i += 1) {
const { metric = {}, queryName = '', legend = '' } = data[i] || {};
const label = getLabelName(metric, queryName || '', legend || '');
// Check if label exists in colorMapping
const color =
colorMapping[label] ||
generateColor(
label,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
const series = getGraphSeries(color, label);
configurations.push(series);
}
return configurations;
};
return { getCustomSeries };
};

View File

@@ -12,6 +12,7 @@ export function useNavigateToTraces(): (
filters: TagFilterItem[],
startTime?: number,
endTime?: number,
sameTab?: boolean,
) => void {
const { currentQuery } = useQueryBuilder();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
@@ -38,7 +39,12 @@ export function useNavigateToTraces(): (
);
return useCallback(
(filters: TagFilterItem[], startTime?: number, endTime?: number): void => {
(
filters: TagFilterItem[],
startTime?: number,
endTime?: number,
sameTab?: boolean,
): void => {
const urlParams = new URLSearchParams();
if (startTime && endTime) {
urlParams.set(QueryParams.startTime, startTime.toString());
@@ -58,7 +64,7 @@ export function useNavigateToTraces(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(newTraceExplorerPath, '_blank');
window.open(newTraceExplorerPath, sameTab ? '_self' : '_blank');
},
[minTime, maxTime, prepareQuery],
);

View File

@@ -133,7 +133,7 @@
.go-to-docs {
display: flex;
flex-direction: column;
gap: 64px;
gap: 44px;
&__container {
display: flex;
flex-direction: column;

View File

@@ -0,0 +1,117 @@
.signoz-modal {
.ant-modal-content {
padding: 0;
background: var(--bg-ink-400);
border: 1px solid var(--bg-slate-500);
}
.ant-modal-header {
background: var(--bg-ink-400);
border-bottom: none;
padding: 12px 24px 8px;
border-bottom: 1px solid var(--bg-slate-500);
margin-bottom: 0;
}
.ant-modal-close {
top: 15px;
height: 14px;
width: 14px;
.ant-modal-close-x {
font-size: 12px;
}
}
.ant-modal-title {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.ant-modal-body {
padding: 16px 24px 24px;
}
.ant-modal-footer {
padding: 24px;
display: flex;
justify-content: end;
gap: 12px;
}
.ant-typography {
color: var(--bg-vanilla-100);
}
.ant-select {
border-radius: 2px !important;
&-selector {
background: var(--bg-ink-300) !important;
border: 1px solid var(--bg-slate-400) !important;
border-radius: 2px !important;
}
}
.ant-select-dropdown {
background: var(--bg-ink-400);
border: 1px solid var(--bg-ink-300);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
}
.ant-select-item {
color: var(--bg-vanilla-100);
&-option-selected {
background: var(--bg-ink-300);
}
}
}
.lightMode {
.signoz-modal {
.ant-modal-content {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
.ant-modal-header {
background: var(--bg-vanilla-100);
border-bottom-color: var(--bg-vanilla-300);
}
.ant-modal-title {
color: var(--bg-ink-500);
}
.ant-typography {
color: var(--bg-ink-500);
}
.ant-select {
&-selector {
background: var(--bg-vanilla-100) !important;
border-color: var(--bg-vanilla-300) !important;
}
}
.ant-select-dropdown {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
.ant-select-item {
color: var(--bg-ink-400);
&-option-selected {
background: var(--bg-vanilla-300);
color: var(--bg-ink-500);
}
&-option-active {
background: var(--bg-vanilla-200);
}
}
}
}

View File

@@ -0,0 +1,25 @@
import './SignozModal.style.scss';
import { Modal, ModalProps } from 'antd';
function SignozModal({
children,
rootClassName = '',
...rest
}: ModalProps): JSX.Element {
return (
<Modal
centered
width={672}
cancelText="Close"
rootClassName={`signoz-modal ${rootClassName}`}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
>
{children}
</Modal>
);
}
export default SignozModal;

View File

@@ -23,4 +23,5 @@ export enum FeatureKeys {
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
AWS_INTEGRATION = 'AWS_INTEGRATION',
}

View File

@@ -32,4 +32,15 @@ export const REACT_QUERY_KEY = {
GET_JOB_LIST: 'GET_JOB_LIST',
GET_DAEMONSET_LIST: 'GET_DAEMONSET_LIST,',
GET_VOLUME_LIST: 'GET_VOLUME_LIST',
// AWS Integration Query Keys
AWS_ACCOUNTS: 'AWS_ACCOUNTS',
AWS_SERVICES: 'AWS_SERVICES',
AWS_SERVICE_DETAILS: 'AWS_SERVICE_DETAILS',
AWS_ACCOUNT_STATUS: 'AWS_ACCOUNT_STATUS',
AWS_UPDATE_ACCOUNT_CONFIG: 'AWS_UPDATE_ACCOUNT_CONFIG',
AWS_UPDATE_SERVICE_CONFIG: 'AWS_UPDATE_SERVICE_CONFIG',
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
AWS_GET_CONNECTION_PARAMS: 'AWS_GET_CONNECTION_PARAMS',
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
};

View File

@@ -59,12 +59,15 @@ const ROUTES = {
WORKSPACE_SUSPENDED: '/workspace-suspended',
SHORTCUTS: '/shortcuts',
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
MESSAGING_QUEUES_OVERVIEW: '/messaging-queues/overview',
METRICS_EXPLORER: '/metrics-explorer/summary',
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
} as const;
export default ROUTES;

View File

@@ -259,7 +259,7 @@ function AnomalyAlertEvaluationView({
grid: {
show: true,
},
axes: getAxes(isDarkMode, yAxisUnit),
axes: getAxes({ isDarkMode, yAxisUnit }),
tzDate: (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
};

View File

@@ -22,6 +22,7 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App';
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { Helmet } from 'react-helmet-async';
@@ -249,7 +250,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (
!isFetchingActiveLicenseV3 &&
!isNull(activeLicenseV3) &&
activeLicenseV3?.event_queue?.event === LicenseEvent.FAILED_PAYMENT
activeLicenseV3?.event_queue?.event === LicenseEvent.DEFAULT
) {
setShowPaymentFailedWarning(true);
}
@@ -287,11 +288,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
const isMessagingQueues = (): boolean =>
routeKey === 'MESSAGING_QUEUES' ||
routeKey === 'MESSAGING_QUEUES_DETAIL' ||
routeKey === 'MESSAGING_QUEUES_KAFKA' ||
routeKey === 'MESSAGING_QUEUES_KAFKA_DETAIL' ||
routeKey === 'MESSAGING_QUEUES_CELERY_TASK' ||
routeKey === 'MESSAGING_QUEUES_OVERVIEW';
const isCloudIntegrationPage = (): boolean =>
routeKey === 'INTEGRATIONS' &&
new URLSearchParams(window.location.search).get('integration') ===
INTEGRATION_TYPES.AWS_INTEGRATION;
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
@@ -426,6 +432,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring()
? 0
: '0 1rem',

View File

@@ -51,14 +51,12 @@ describe('BillingContainer', () => {
expect(cost).toBeInTheDocument();
const dayRemainingInBillingPeriod = await screen.findByText(
/11 days_remaining/i,
/Please upgrade plan now to retain your data./i,
);
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
const manageBilling = screen.getByRole('button', {
name: 'manage_billing',
});
expect(manageBilling).toBeInTheDocument();
const upgradePlanButton = screen.getByTestId('upgrade-plan-button');
expect(upgradePlanButton).toBeInTheDocument();
const dollar = screen.getByText(/\$1,278.3/i);
await waitFor(() => expect(dollar).toBeInTheDocument());

View File

@@ -202,15 +202,16 @@ export default function BillingContainer(): JSX.Element {
const isSubscriptionPastDue =
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id],
{
queryFn: () => getUsage(activeLicense?.key || ''),
onError: handleError,
enabled: activeLicense !== null,
onSuccess: processUsageData,
},
);
const {
isLoading,
isFetching: isFetchingBillingData,
data: billingData,
} = useQuery([REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id], {
queryFn: () => getUsage(activeLicense?.key || ''),
onError: handleError,
enabled: activeLicense !== null,
onSuccess: processUsageData,
});
useEffect(() => {
const activeValidLicense =
@@ -318,7 +319,7 @@ export default function BillingContainer(): JSX.Element {
});
const handleBilling = useCallback(async () => {
if (isFreeTrial && !licenses?.trialConvertedToSubscription) {
if (!licenses?.trialConvertedToSubscription) {
logEvent('Billing : Upgrade Plan', {
user: pick(user, ['email', 'userId', 'name']),
org,
@@ -411,6 +412,12 @@ export default function BillingContainer(): JSX.Element {
}
}, [apiResponse, notifications]);
const showGracePeriodMessage =
!isLoading &&
!licenses?.trialConvertedToSubscription &&
!licenses?.onTrial &&
licenses?.gracePeriodEnd;
return (
<div className="billing-container">
<Flex vertical style={{ marginBottom: 16 }}>
@@ -433,7 +440,8 @@ export default function BillingContainer(): JSX.Element {
{isCloudUserVal ? t('teams_cloud') : t('teams')}{' '}
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title>
{!isLoading && !isFetchingBillingData ? (
{!isLoading && !isFetchingBillingData && !showGracePeriodMessage ? (
<Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
{daysRemaining} {daysRemainingStr}
</Typography.Text>
@@ -451,15 +459,16 @@ export default function BillingContainer(): JSX.Element {
Download CSV
</Button>
<Button
data-testid="header-billing-button"
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
>
{isFreeTrial && !licenses?.trialConvertedToSubscription
? t('upgrade_plan')
: t('manage_billing')}
{licenses?.trialConvertedToSubscription
? t('manage_billing')
: t('upgrade_plan')}
</Button>
</Flex>
</Flex>
@@ -473,18 +482,35 @@ export default function BillingContainer(): JSX.Element {
</Typography.Text>
)}
{!isLoading && !isFetchingBillingData ? (
headerText && (
<Alert
message={headerText}
type="info"
showIcon
style={{ marginTop: 12 }}
/>
)
) : (
{!isLoading && !isFetchingBillingData && !showGracePeriodMessage
? headerText && (
<Alert
message={headerText}
type="info"
showIcon
style={{ marginTop: 12 }}
/>
)
: null}
{isLoading || isFetchingBillingData ? (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
)}
) : null}
{!isLoading &&
!isFetchingBillingData &&
billingData &&
licenses?.gracePeriodEnd &&
showGracePeriodMessage ? (
<Alert
message={`Your data is safe with us until ${getFormattedDate(
licenses?.gracePeriodEnd || Date.now(),
)}. Please upgrade plan now to retain your data.`}
type="info"
showIcon
style={{ marginTop: 12 }}
/>
) : null}
{isSubscriptionPastDue &&
(!isLoading && !isFetchingBillingData ? (
@@ -514,7 +540,7 @@ export default function BillingContainer(): JSX.Element {
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
</div>
{isFreeTrial && !licenses?.trialConvertedToSubscription && (
{!licenses?.trialConvertedToSubscription && (
<div className="upgrade-plan-benefits">
<Row
justify="space-between"
@@ -552,6 +578,7 @@ export default function BillingContainer(): JSX.Element {
</Col>
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
data-testid="upgrade-plan-button"
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}

View File

@@ -122,7 +122,7 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
[graphCompatibleData.data.result],
);
const axesOptions = getAxes(isDarkMode, '');
const axesOptions = getAxes({ isDarkMode, yAxisUnit: '' });
const optionsForChart: uPlot.Options = useMemo(
() => ({

View File

@@ -0,0 +1,15 @@
import Header from './Header/Header';
import HeroSection from './HeroSection/HeroSection';
import ServicesTabs from './ServicesSection/ServicesTabs';
function CloudIntegrationPage(): JSX.Element {
return (
<div>
<Header />
<HeroSection />
<ServicesTabs />
</div>
);
}
export default CloudIntegrationPage;

View File

@@ -0,0 +1,65 @@
.cloud-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 18px;
border-bottom: 1px solid var(--bg-slate-400);
&__navigation {
display: flex;
align-items: center;
padding: 6px 0px 6px;
}
&__breadcrumb-link {
display: flex;
align-items: center;
gap: 8px;
}
&__breadcrumb-title {
color: var(--bg-vanilla-400);
font-size: 14px;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
&__help {
display: flex;
align-items: center;
justify-content: center;
padding: 6px;
gap: 6px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
border-radius: 2px;
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
width: 113px;
height: 32px;
cursor: pointer;
}
}
.lightMode {
.cloud-header {
border-bottom: 1px solid var(--bg-slate-300);
&__breadcrumb-title {
color: var(--bg-ink-400);
}
&__help {
border-color: var(--bg-slate-300);
background: var(--bg-vanilla-100);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-slate-400);
color: var(--bg-ink-500);
}
}
}
}

View File

@@ -0,0 +1,45 @@
import './Header.styles.scss';
import Breadcrumb from 'antd/es/breadcrumb';
import ROUTES from 'constants/routes';
import { Blocks, LifeBuoy } from 'lucide-react';
import { Link } from 'react-router-dom';
function Header(): JSX.Element {
return (
<div className="cloud-header">
<div className="cloud-header__navigation">
<Breadcrumb
className="cloud-header__breadcrumb"
items={[
{
title: (
<Link to={ROUTES.INTEGRATIONS}>
<span className="cloud-header__breadcrumb-link">
<Blocks size={16} color="var(--bg-vanilla-400)" />
<span className="cloud-header__breadcrumb-title">Integrations</span>
</span>
</Link>
),
},
{
title: (
<div className="cloud-header__breadcrumb-title">
Amazon Web Services
</div>
),
},
]}
/>
</div>
<div className="cloud-header__actions">
<button className="cloud-header__help" type="button">
<LifeBuoy size={12} />
Get Help
</button>
</div>
</div>
);
}
export default Header;

View File

@@ -0,0 +1,72 @@
.hero-section {
height: 308px;
padding: 26px 16px;
display: flex;
gap: 24px;
position: relative;
overflow: hidden;
background-position: right;
background-size: cover;
background-repeat: no-repeat;
border-bottom: 1px solid var(--bg-slate-500);
&__icon {
height: fit-content;
background-color: var(--bg-ink-400);
padding: 12px;
border: 1px solid var(--bg-ink-300);
border-radius: 6px;
width: 60px;
height: 60px;
display: flex;
align-items: center;
img {
width: 100%;
}
}
&__details {
display: flex;
flex-direction: column;
gap: 12px;
.title {
color: var(--bg-vanilla-100);
font-size: 24px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.12px;
}
.description {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
}
}
.lightMode {
.hero-section {
border-bottom: 1px solid var(--bg-vanilla-300);
&__icon {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
&__details {
.title {
color: var(--bg-ink-500);
}
.description {
color: var(--bg-ink-400);
}
}
}
}
.integrations-select {
height: 44px;
}

View File

@@ -0,0 +1,34 @@
import './HeroSection.style.scss';
import { useIsDarkMode } from 'hooks/useDarkMode';
import AccountActions from './components/AccountActions';
function HeroSection(): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div
className="hero-section"
style={
isDarkMode
? {
backgroundImage: `url('/Images/integrations-hero-bg.png')`,
}
: {}
}
>
<div className="hero-section__icon">
<img src="/Logos/aws-dark.svg" alt="aws-logo" />
</div>
<div className="hero-section__details">
<div className="title">Amazon Web Services</div>
<div className="description">
One-click setup for AWS monitoring with SigNoz
</div>
<AccountActions />
</div>
</div>
);
}
export default HeroSection;

View File

@@ -0,0 +1,142 @@
.hero-section {
&__actions {
margin-top: 12px;
&-with-account {
display: flex;
flex-direction: column;
gap: 10px;
}
}
&__input-skeleton {
width: 300px;
margin-bottom: 16px;
}
&__new-account-button-skeleton {
width: 180px;
margin-right: 8px;
}
&__account-settings-button-skeleton {
width: 140px;
}
&__action-buttons {
display: flex;
align-items: center;
gap: 8px;
}
&__action-button {
font-family: 'Inter';
border-radius: 2px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
line-height: 16px;
padding: 8px 17px;
&.primary {
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
}
&.secondary {
display: flex;
align-items: center;
border: 1px solid var(--bg-ink-300);
color: var(--bg-vanilla-100);
border-radius: 2px;
background: var(--bg-slate-400);
box-shadow: none;
}
}
}
.cloud-account-selector {
border-radius: 2px;
border: 1px solid var(--bg-ink-300);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
.ant-select-selector {
border-color: var(--bg-slate-400) !important;
background: var(--bg-ink-300) !important;
padding: 6px 8px !important;
}
.ant-select-selection-item {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
.account-option-item {
display: flex;
align-items: center;
justify-content: space-between;
&__selected {
display: flex;
align-items: center;
justify-content: center;
height: 14px;
width: 14px;
background-color: rgba(192, 193, 195, 0.2); /* #C0C1C3 with 0.2 opacity */
border-radius: 2px;
}
}
}
.lightMode {
.hero-section__action-button {
&.primary {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
}
&.secondary {
border-color: var(--bg-vanilla-300);
color: var(--bg-ink-400);
background: var(--bg-vanilla-100);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
}
.cloud-account-selector {
background: var(--bg-vanilla-100);
.ant-select-selector {
background: var(--bg-vanilla-100) !important;
border-color: var(--bg-vanilla-400) !important;
}
.ant-select-item-option-active {
background: var(--bg-vanilla-400) !important;
}
.ant-select-selection-item {
color: var(--bg-ink-400);
}
&:hover {
.ant-select-selector {
border-color: var(--bg-vanilla-400) !important;
}
}
}
.account-option-item {
color: var(--bg-ink-400);
&__selected {
background: var(--bg-robin-500);
}
}
}

View File

@@ -0,0 +1,219 @@
import './AccountActions.style.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, ChevronDown } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import { CloudAccount } from '../../ServicesSection/types';
import AccountSettingsModal from './AccountSettingsModal';
import CloudAccountSetupModal from './CloudAccountSetupModal';
interface AccountOptionItemProps {
label: React.ReactNode;
isSelected: boolean;
}
function AccountOptionItem({
label,
isSelected,
}: AccountOptionItemProps): JSX.Element {
return (
<div className="account-option-item">
{label}
{isSelected && (
<div className="account-option-item__selected">
<Check size={12} color={Color.BG_VANILLA_100} />
</div>
)}
</div>
);
}
function renderOption(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
option: any,
activeAccountId: string | undefined,
): JSX.Element {
return (
<AccountOptionItem
label={option.label}
isSelected={option.value === activeAccountId}
/>
);
}
const getAccountById = (
accounts: CloudAccount[],
accountId: string,
): CloudAccount | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;
function AccountActionsRenderer({
accounts,
isLoading,
activeAccount,
selectOptions,
onAccountChange,
onIntegrationModalOpen,
onAccountSettingsModalOpen,
}: {
accounts: CloudAccount[] | undefined;
isLoading: boolean;
activeAccount: CloudAccount | null;
selectOptions: SelectProps['options'];
onAccountChange: (value: string) => void;
onIntegrationModalOpen: () => void;
onAccountSettingsModalOpen: () => void;
}): JSX.Element {
if (isLoading) {
return (
<div className="hero-section__actions-with-account">
<Skeleton.Input
active
size="large"
block
className="hero-section__input-skeleton"
/>
<div className="hero-section__action-buttons">
<Skeleton.Button
active
size="large"
className="hero-section__new-account-button-skeleton"
/>
<Skeleton.Button
active
size="large"
className="hero-section__account-settings-button-skeleton"
/>
</div>
</div>
);
}
if (accounts?.length) {
return (
<div className="hero-section__actions-with-account">
<Select
value={`Account: ${activeAccount?.cloud_account_id}`}
options={selectOptions}
rootClassName="cloud-account-selector"
placeholder="Select AWS Account"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
optionRender={(option): JSX.Element =>
renderOption(option, activeAccount?.cloud_account_id)
}
onChange={onAccountChange}
/>
<div className="hero-section__action-buttons">
<Button
type="primary"
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
onClick={onAccountSettingsModalOpen}
>
Account Settings
</Button>
</div>
</div>
);
}
return (
<Button
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
Integrate Now
</Button>
);
}
function AccountActions(): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: accounts, isLoading } = useAwsAccounts();
const initialAccount = useMemo(
() =>
accounts?.length
? getAccountById(accounts, urlQuery.get('cloudAccountId') || '') ||
accounts[0]
: null,
[accounts, urlQuery],
);
const [activeAccount, setActiveAccount] = useState<CloudAccount | null>(
initialAccount,
);
// Update state when initial value changes
useEffect(() => {
if (initialAccount !== null) {
setActiveAccount(initialAccount);
urlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: urlQuery.toString() });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialAccount]);
const [isIntegrationModalOpen, setIsIntegrationModalOpen] = useState(false);
const [isAccountSettingsModalOpen, setIsAccountSettingsModalOpen] = useState(
false,
);
const selectOptions: SelectProps['options'] = useMemo(
() =>
accounts?.length
? accounts.map((account) => ({
value: account.cloud_account_id,
label: account.cloud_account_id,
}))
: [],
[accounts],
);
return (
<div className="hero-section__actions">
<AccountActionsRenderer
accounts={accounts}
isLoading={isLoading}
activeAccount={activeAccount}
selectOptions={selectOptions}
onAccountChange={(value): void => {
if (accounts) {
setActiveAccount(getAccountById(accounts, value));
urlQuery.set('cloudAccountId', value);
navigate({ search: urlQuery.toString() });
}
}}
onIntegrationModalOpen={(): void => setIsIntegrationModalOpen(true)}
onAccountSettingsModalOpen={(): void => setIsAccountSettingsModalOpen(true)}
/>
{isIntegrationModalOpen && (
<CloudAccountSetupModal
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
)}
{isAccountSettingsModalOpen && (
<AccountSettingsModal
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
/>
)}
</div>
);
}
export default AccountActions;

View File

@@ -0,0 +1,189 @@
.account-settings-modal {
&__title-account-id {
color: var(--bg-vanilla-100);
font-family: 'Geist Mono';
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
&__body {
display: flex;
flex-direction: column;
gap: 17px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
padding: 14px;
&-account-info {
&-connected-account-details {
&-title {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
&-account-id {
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
&-account-id {
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 700;
line-height: 18px;
letter-spacing: -0.06px;
}
}
}
}
&-regions-switch {
display: flex;
flex-direction: column;
gap: 10px;
&-title {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
&-switch {
display: flex;
align-items: center;
gap: 10px;
&-label {
color: var(--bg-vanilla-400);
background-color: transparent;
border: none;
font-family: Inter;
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.005em;
cursor: pointer;
}
}
}
&-regions-select {
margin-top: 8px;
}
}
&__footer {
&-close-button,
&-save-button {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 12px;
font-weight: 500;
padding-left: 16px;
padding-right: 16px;
}
&-close-button {
border-radius: 2px;
background: var(--bg-slate-400);
border: none;
}
&-save-button {
&:disabled {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
opacity: 0.6;
border: none;
}
border-radius: 2px;
margin: 0 !important;
}
}
.ant-modal-body {
padding-bottom: 0 !important;
}
.ant-modal-footer {
margin: 0;
padding: 24px 24px 12px;
}
.integration-detail-content {
margin: 0;
}
}
.lightMode {
.account-settings-modal {
&__title-account-id {
color: var(--bg-ink-500);
}
&__body {
border-color: var(--bg-vanilla-300);
&-account-info {
&-connected-account-details {
&-title {
color: var(--bg-ink-500);
}
&-account-id {
color: var(--bg-ink-400);
&-account-id {
color: var(--bg-ink-500);
}
}
}
}
&-regions-switch {
&-title {
color: var(--bg-ink-500);
}
&-switch {
&-label {
color: var(--bg-ink-400);
&:hover {
color: var(--bg-ink-500);
}
}
}
}
}
&__footer {
&-close-button,
&-save-button {
color: var(--bg-vanilla-100);
}
&-close-button {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
&-save-button {
// Keep primary button same as dark mode
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
&:disabled {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
opacity: 0.6;
}
&:not(:disabled):hover {
background: var(--bg-robin-400);
}
}
}
}
}

View File

@@ -0,0 +1,188 @@
import './AccountSettingsModal.style.scss';
import { Form, Select, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
getRegionPreviewText,
useAccountSettingsModal,
} from 'hooks/integrations/aws/useAccountSettingsModal';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Dispatch, SetStateAction, useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { CloudAccount } from '../../ServicesSection/types';
import { RegionSelector } from './RegionSelector';
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
interface AccountSettingsModalProps {
onClose: () => void;
account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
}
function AccountSettingsModal({
onClose,
account,
setActiveAccount,
}: AccountSettingsModalProps): JSX.Element {
const {
form,
isLoading,
selectedRegions,
includeAllRegions,
isRegionSelectOpen,
isSaveDisabled,
setSelectedRegions,
setIncludeAllRegions,
setIsRegionSelectOpen,
handleIncludeAllRegionsChange,
handleSubmit,
handleClose,
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
const queryClient = useQueryClient();
const urlQuery = useUrlQuery();
const handleRemoveIntegrationAccountSuccess = (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
urlQuery.delete('cloudAccountId');
handleClose();
history.replace({ search: urlQuery.toString() });
};
const renderRegionSelector = useCallback(() => {
if (isRegionSelectOpen) {
return (
<RegionSelector
selectedRegions={selectedRegions}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
/>
);
}
return (
<>
<div className="account-settings-modal__body-regions-switch-switch ">
<Switch
checked={includeAllRegions}
onChange={handleIncludeAllRegionsChange}
/>
<button
className="account-settings-modal__body-regions-switch-switch-label"
type="button"
onClick={(): void => handleIncludeAllRegionsChange(!includeAllRegions)}
>
Include all regions
</button>
</div>
<Select
suffixIcon={null}
placeholder="Select Region(s)"
className="cloud-account-setup-form__select account-settings-modal__body-regions-select integrations-select"
onClick={(): void => setIsRegionSelectOpen(true)}
mode="multiple"
maxTagCount={3}
value={getRegionPreviewText(selectedRegions)}
open={false}
/>
</>
);
}, [
isRegionSelectOpen,
selectedRegions,
includeAllRegions,
handleIncludeAllRegionsChange,
setIsRegionSelectOpen,
setSelectedRegions,
setIncludeAllRegions,
]);
const renderAccountDetails = useCallback(
() => (
<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">
AWS Account:{' '}
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
{account?.id}
</span>
</div>
</div>
</div>
),
[account?.id],
);
const modalTitle = (
<div className="account-settings-modal__title">
Account settings for{' '}
<span className="account-settings-modal__title-account-id">
{account?.id}
</span>
</div>
);
return (
<SignozModal
open
title={modalTitle}
onCancel={handleClose}
onOk={handleSubmit}
okText="Save"
okButtonProps={{
disabled: isSaveDisabled,
className: 'account-settings-modal__footer-save-button',
loading: isLoading,
}}
cancelButtonProps={{
className: 'account-settings-modal__footer-close-button',
}}
width={672}
rootClassName="account-settings-modal"
>
<Form
form={form}
layout="vertical"
initialValues={{
selectedRegions,
includeAllRegions,
}}
>
<div className="account-settings-modal__body">
{renderAccountDetails()}
<Form.Item
name="selectedRegions"
rules={[
{
validator: async (): Promise<void> => {
if (selectedRegions.length === 0) {
throw new Error('Please select at least one region to monitor');
}
},
message: 'Please select at least one region to monitor',
},
]}
>
{renderRegionSelector()}
</Form.Item>
<div className="integration-detail-content">
<RemoveIntegrationAccount
accountId={account?.id}
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
/>
</div>
</div>
</Form>
</SignozModal>
);
}
export default AccountSettingsModal;

View File

@@ -0,0 +1,53 @@
import { Color } from '@signozhq/design-tokens';
import { Alert, Spin } from 'antd';
import { LoaderCircle, TriangleAlert } from 'lucide-react';
import { ModalStateEnum } from '../types';
function AlertMessage({
modalState,
}: {
modalState: ModalStateEnum;
}): JSX.Element | null {
switch (modalState) {
case ModalStateEnum.WAITING:
return (
<Alert
message={
<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"
/>
}
/>
Waiting for connection, retrying in{' '}
<span className="retry-time">10</span> secs...
</div>
}
className="cloud-account-setup-form__alert"
type="warning"
/>
);
case ModalStateEnum.ERROR:
return (
<Alert
message={
<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:
return null;
}
}
export default AlertMessage;

View File

@@ -0,0 +1,221 @@
.cloud-account-setup-modal {
.account-setup-modal-footer {
&__confirm-button {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
}
&__confirm-selection-count {
font-family: 'Geist Mono';
}
&__close-button {
background: var(--bg-slate-400);
border-radius: 2px;
color: var(--bg-vanilla-100);
font-family: 'Inter';
font-size: 12px;
font-weight: 500;
}
}
.cloud-account-setup-form {
.disabled {
opacity: 0.4;
}
&,
&__content {
display: flex;
flex-direction: column;
gap: 38px;
}
&__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(--bg-sakura-400);
border: 1px solid rgba(242, 71, 105, 0.1);
background: rgba(242, 71, 105, 0.1);
}
&.ant-alert-warning {
color: var(--bg-amber-400);
border: 1px solid rgba(255, 205, 86, 0.1);
background: rgba(255, 205, 86, 0.1);
}
&-message {
display: flex;
align-items: center;
gap: 8px;
.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(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
&__description {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
&__select {
.ant-select-selection-item {
color: var(--bg-vanilla-100);
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(--bg-vanilla-400);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
margin-bottom: 12px;
&-label {
background-color: transparent;
border: none;
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
cursor: pointer;
}
}
&__note {
padding: 12px;
color: var(--bg-robin-400);
font-size: 12px;
line-height: 22px;
letter-spacing: -0.06px;
border-radius: 4px;
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
}
&__submit-button {
border-radius: 2px;
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
&-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
&:disabled {
opacity: 0.4;
}
}
}
}
.lightMode {
.cloud-account-setup-modal {
.account-setup-modal-footer {
&__confirm-button {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
}
&__close-button {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
}
.cloud-account-setup-form {
&__title {
color: var(--bg-ink-500);
}
&__description {
color: var(--bg-ink-400);
}
&__select {
.ant-select-selection-item {
color: var(--bg-ink-500);
}
}
&__include-all-regions-switch {
color: var(--bg-ink-400);
&-label {
color: var(--bg-ink-400);
&:hover {
color: var(--bg-ink-500);
}
}
}
&__note {
color: var(--bg-robin-500);
border: 1px solid rgba(78, 116, 248, 0.2);
background: rgba(78, 116, 248, 0.1);
}
&__submit-button {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
}
&__alert {
&.ant-alert-error {
color: var(--bg-cherry-500);
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
}
&.ant-alert-warning {
color: var(--bg-amber-500);
border: 1px solid rgba(255, 205, 86, 0.2);
background: rgba(255, 205, 86, 0.1);
}
&-message {
.retry-time {
color: var(--bg-ink-500);
}
}
}
}
}
}

View File

@@ -0,0 +1,183 @@
import './CloudAccountSetupModal.style.scss';
import { Color } from '@signozhq/design-tokens';
import SignozModal from 'components/SignozModal/SignozModal';
import { useIntegrationModal } from 'hooks/integrations/aws/useIntegrationModal';
import { SquareArrowOutUpRight } from 'lucide-react';
import { useCallback } from 'react';
import {
ActiveViewEnum,
IntegrationModalProps,
ModalStateEnum,
} from '../types';
import { RegionForm } from './RegionForm';
import { RegionSelector } from './RegionSelector';
import { SuccessView } from './SuccessView';
function CloudAccountSetupModal({
onClose,
}: IntegrationModalProps): JSX.Element {
const {
form,
modalState,
setModalState,
isLoading,
activeView,
selectedRegions,
includeAllRegions,
isGeneratingUrl,
setSelectedRegions,
setIncludeAllRegions,
handleIncludeAllRegionsChange,
handleRegionSelect,
handleSubmit,
handleClose,
setActiveView,
allRegions,
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
} = useIntegrationModal({ onClose });
const renderContent = useCallback(() => {
if (modalState === ModalStateEnum.SUCCESS) {
return <SuccessView />;
}
if (activeView === ActiveViewEnum.SELECT_REGIONS) {
return (
<RegionSelector
selectedRegions={selectedRegions}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
/>
);
}
return (
<RegionForm
form={form}
modalState={modalState}
setModalState={setModalState}
selectedRegions={selectedRegions}
includeAllRegions={includeAllRegions}
onIncludeAllRegionsChange={handleIncludeAllRegionsChange}
onRegionSelect={handleRegionSelect}
onSubmit={handleSubmit}
accountId={accountId}
selectedDeploymentRegion={selectedDeploymentRegion}
handleRegionChange={handleRegionChange}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
/>
);
}, [
modalState,
activeView,
form,
setModalState,
selectedRegions,
includeAllRegions,
handleIncludeAllRegionsChange,
handleRegionSelect,
handleSubmit,
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
setSelectedRegions,
setIncludeAllRegions,
]);
const getSelectedRegionsCount = useCallback(
(): number =>
selectedRegions.includes('all') ? allRegions.length : selectedRegions.length,
[selectedRegions, allRegions],
);
const getModalConfig = useCallback(() => {
// Handle success state first
if (modalState === ModalStateEnum.SUCCESS) {
return {
title: 'AWS Webservice Integration',
okText: (
<div className="cloud-account-setup-success-view__footer-button">
Continue
</div>
),
block: true,
onOk: handleClose,
cancelButtonProps: { style: { display: 'none' } },
disabled: false,
};
}
// Handle other views
const viewConfigs = {
[ActiveViewEnum.FORM]: {
title: 'Add AWS Account',
okText: (
<div className="cloud-account-setup-form__submit-button-content">
Launch Cloud Formation Template{' '}
<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />
</div>
),
onOk: handleSubmit,
disabled:
isLoading || isGeneratingUrl || modalState === ModalStateEnum.WAITING,
cancelButtonProps: { style: { display: 'none' } },
},
[ActiveViewEnum.SELECT_REGIONS]: {
title: 'Which regions do you want to monitor?',
okText: `Confirm Selection (${getSelectedRegionsCount()})`,
onOk: (): void => setActiveView(ActiveViewEnum.FORM),
isLoading: isLoading || isGeneratingUrl,
cancelButtonProps: { style: { display: 'block' } },
disabled: false,
},
};
return viewConfigs[activeView];
}, [
modalState,
handleSubmit,
getSelectedRegionsCount,
isLoading,
isGeneratingUrl,
activeView,
handleClose,
setActiveView,
]);
const modalConfig = getModalConfig();
return (
<SignozModal
open
className="cloud-account-setup-modal"
title={modalConfig.title}
onCancel={handleClose}
onOk={modalConfig.onOk}
okText={modalConfig.okText}
okButtonProps={{
loading: isLoading,
disabled: selectedRegions.length === 0 || modalConfig.disabled,
className:
activeView === ActiveViewEnum.FORM
? 'cloud-account-setup-form__submit-button'
: 'account-setup-modal-footer__confirm-button',
block: activeView === ActiveViewEnum.FORM,
}}
cancelButtonProps={modalConfig.cancelButtonProps}
width={672}
>
{renderContent()}
</SignozModal>
);
}
export default CloudAccountSetupModal;

View File

@@ -0,0 +1,137 @@
import { Color } from '@signozhq/design-tokens';
import { Form, Select, Switch } from 'antd';
import { ChevronDown } from 'lucide-react';
import { Region } from 'utils/regions';
// Form section components
function RegionDeploymentSection({
regions,
selectedDeploymentRegion,
handleRegionChange,
isFormDisabled,
}: {
regions: Region[];
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
isFormDisabled: boolean;
}): JSX.Element {
return (
<div className="cloud-account-setup-form__form-group">
<div className="cloud-account-setup-form__title">
Where should we deploy the SigNoz Cloud stack?
</div>
<div className="cloud-account-setup-form__description">
Choose the AWS region for CloudFormation stack 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. US East (N. Virginia)"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
className="cloud-account-setup-form__select integrations-select"
onChange={handleRegionChange}
value={selectedDeploymentRegion}
disabled={isFormDisabled}
>
{regions.flatMap((region) =>
region.subRegions.map((subRegion) => (
<Select.Option key={subRegion.id} value={subRegion.id}>
{subRegion.displayName}
</Select.Option>
)),
)}
</Select>
</Form.Item>
</div>
);
}
function MonitoringRegionsSection({
includeAllRegions,
selectedRegions,
onIncludeAllRegionsChange,
getRegionPreviewText,
onRegionSelect,
isFormDisabled,
}: {
includeAllRegions: boolean;
selectedRegions: string[];
onIncludeAllRegionsChange: (checked: boolean) => void;
getRegionPreviewText: (regions: string[]) => string[];
onRegionSelect: () => void;
isFormDisabled: boolean;
}): JSX.Element {
return (
<div className="cloud-account-setup-form__form-group">
<div className="cloud-account-setup-form__title">
Which regions do you want to monitor?
</div>
<div className="cloud-account-setup-form__description">
Choose only the regions you want SigNoz to monitor. You can enable all at
once, or pick specific ones:
</div>
<Form.Item
name="monitorRegions"
rules={[
{
validator: async (): Promise<void> => {
if (selectedRegions.length === 0) {
return Promise.reject();
}
return Promise.resolve();
},
message: 'Please select at least one region to monitor',
},
]}
className="cloud-account-setup-form__form-item"
>
<div className="cloud-account-setup-form__include-all-regions-switch">
<Switch
size="small"
checked={includeAllRegions}
onChange={onIncludeAllRegionsChange}
disabled={isFormDisabled}
/>
<button
className="cloud-account-setup-form__include-all-regions-switch-label"
type="button"
onClick={(): void =>
!isFormDisabled
? onIncludeAllRegionsChange(!includeAllRegions)
: undefined
}
>
Include all regions
</button>
</div>
<Select
suffixIcon={null}
placeholder="Select Region(s)"
className="cloud-account-setup-form__select integrations-select"
onClick={!isFormDisabled ? onRegionSelect : undefined}
mode="multiple"
maxTagCount={3}
value={getRegionPreviewText(selectedRegions)}
open={false}
/>
</Form.Item>
</div>
);
}
function ComplianceNote(): JSX.Element {
return (
<div className="cloud-account-setup-form__form-group">
<div className="cloud-account-setup-form__note">
Note: Some organizations may require the CloudFormation stack to be deployed
in the same region as their primary infrastructure for compliance or latency
reasons.
</div>
</div>
);
}
export { ComplianceNote, MonitoringRegionsSection, RegionDeploymentSection };

View File

@@ -0,0 +1,102 @@
import { Form } from 'antd';
import cx from 'classnames';
import { useAccountStatus } from 'hooks/integrations/aws/useAccountStatus';
import { useRef } from 'react';
import { AccountStatusResponse } from 'types/api/integrations/aws';
import { regions } from 'utils/regions';
import { ModalStateEnum, RegionFormProps } from '../types';
import AlertMessage from './AlertMessage';
import {
ComplianceNote,
MonitoringRegionsSection,
RegionDeploymentSection,
} from './IntegrateNowFormSections';
import RenderConnectionFields from './RenderConnectionParams';
const allRegions = (): string[] =>
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
const getRegionPreviewText = (regions: string[]): string[] => {
if (regions.includes('all')) {
return allRegions();
}
return regions;
};
export function RegionForm({
form,
modalState,
setModalState,
selectedRegions,
includeAllRegions,
onIncludeAllRegionsChange,
onRegionSelect,
onSubmit,
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
}: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
const errorTimeout = 5 * 60 * 1000;
const { isLoading: isAccountStatusLoading } = useAccountStatus(accountId, {
refetchInterval,
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
onSuccess: (data: AccountStatusResponse) => {
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
setModalState(ModalStateEnum.SUCCESS);
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
// 5 minutes in milliseconds
setModalState(ModalStateEnum.ERROR);
}
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
},
});
const isFormDisabled =
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
return (
<Form
form={form}
className="cloud-account-setup-form"
layout="vertical"
onFinish={onSubmit}
>
<AlertMessage modalState={modalState} />
<div
className={cx(`cloud-account-setup-form__content`, {
disabled: isFormDisabled,
})}
>
<RegionDeploymentSection
regions={regions}
handleRegionChange={handleRegionChange}
isFormDisabled={isFormDisabled}
selectedDeploymentRegion={selectedDeploymentRegion}
/>
<MonitoringRegionsSection
includeAllRegions={includeAllRegions}
selectedRegions={selectedRegions}
onIncludeAllRegionsChange={onIncludeAllRegionsChange}
getRegionPreviewText={getRegionPreviewText}
onRegionSelect={onRegionSelect}
isFormDisabled={isFormDisabled}
/>
<ComplianceNote />
<RenderConnectionFields
isConnectionParamsLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
isFormDisabled={isFormDisabled}
/>
</div>
</Form>
);
}

View File

@@ -0,0 +1,21 @@
.select-all {
margin-bottom: 20px;
}
.regions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
gap: 8px;
}
.region-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.region-group label {
display: flex;
gap: 10px;
align-items: center;
}

View File

@@ -0,0 +1,63 @@
import './RegionSelector.style.scss';
import { Checkbox } from 'antd';
import { useRegionSelection } from 'hooks/integrations/aws/useRegionSelection';
import { Dispatch, SetStateAction } from 'react';
import { regions } from 'utils/regions';
export function RegionSelector({
selectedRegions,
setSelectedRegions,
setIncludeAllRegions,
}: {
selectedRegions: string[];
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
}): JSX.Element {
const {
allRegionIds,
handleSelectAll,
handleRegionSelect,
} = useRegionSelection({
selectedRegions,
setSelectedRegions,
setIncludeAllRegions,
});
return (
<div className="region-selector">
<div className="select-all">
<Checkbox
checked={selectedRegions.includes('all')}
indeterminate={
selectedRegions.length > 20 &&
selectedRegions.length < allRegionIds.length
}
onChange={(e): void => handleSelectAll(e.target.checked)}
>
Select All Regions
</Checkbox>
</div>
<div className="regions-grid">
{regions.map((region) => (
<div key={region.id} className="region-group">
<h3>{region.name}</h3>
{region.subRegions.map((subRegion) => (
<Checkbox
key={subRegion.id}
checked={
selectedRegions.includes('all') ||
selectedRegions.includes(subRegion.id)
}
onChange={(): void => handleRegionSelect(subRegion.id)}
>
{subRegion.name}
</Checkbox>
))}
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,48 @@
.remove-integration-account {
display: flex;
justify-content: space-between;
padding: 16px;
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
&__header {
display: flex;
flex-direction: column;
gap: 6px;
}
&__title {
color: var(--bg-cherry-500);
font-size: 14px;
letter-spacing: -0.07px;
}
&__subtitle {
color: var(--bg-cherry-300);
font-size: 14px;
line-height: 22px;
letter-spacing: -0.07px;
}
&__button {
display: flex;
align-items: center;
background: var(--bg-cherry-500);
border: none;
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
line-height: 13.3px; /* 110.833% */
padding: 9px 13px;
.ant-btn-icon {
margin-inline-end: 4px !important;
}
&:hover {
&.ant-btn-default {
color: var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -0,0 +1,94 @@
import './RemoveIntegrationAccount.scss';
import { Button, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import removeAwsIntegrationAccount from 'api/Integrations/removeAwsIntegrationAccount';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react';
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
import { useState } from 'react';
import { useMutation } from 'react-query';
function RemoveIntegrationAccount({
accountId,
onRemoveIntegrationAccountSuccess,
}: {
accountId: string;
onRemoveIntegrationAccountSuccess: () => void;
}): JSX.Element {
const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = (): void => {
setIsModalOpen(true);
};
const {
mutate: removeIntegration,
isLoading: isRemoveIntegrationLoading,
} = useMutation(removeAwsIntegrationAccount, {
onSuccess: () => {
onRemoveIntegrationAccountSuccess?.();
setIsModalOpen(false);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
const handleOk = (): void => {
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
accountId,
});
removeIntegration(accountId);
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
return (
<div className="remove-integration-account">
<div className="remove-integration-account__header">
<div className="remove-integration-account__title">Remove Integration</div>
<div className="remove-integration-account__subtitle">
Removing this integration won&apos;t delete any existing data but will stop
collecting new data from AWS.
</div>
</div>
<Button
className="remove-integration-account__button"
icon={<X size={14} />}
onClick={(): void => showModal()}
>
Remove
</Button>
<Modal
className="remove-integration-modal"
open={isModalOpen}
title="Remove integration"
onOk={handleOk}
onCancel={handleCancel}
okText="Remove Integration"
okButtonProps={{
danger: true,
disabled: isRemoveIntegrationLoading,
}}
>
<div className="remove-integration-modal__text">
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.
</div>
</Modal>
</div>
);
}
export default RemoveIntegrationAccount;

View File

@@ -0,0 +1,61 @@
import { Form, Input } from 'antd';
import { ConnectionParams } from 'types/api/integrations/aws';
function RenderConnectionFields({
isConnectionParamsLoading,
connectionParams,
isFormDisabled,
}: {
isConnectionParamsLoading?: boolean;
connectionParams?: ConnectionParams | null;
isFormDisabled?: boolean;
}): JSX.Element | null {
if (
isConnectionParamsLoading ||
(!!connectionParams?.ingestion_url &&
!!connectionParams?.ingestion_key &&
!!connectionParams?.signoz_api_url)
) {
return null;
}
return (
<Form.Item name="connection_params">
{!connectionParams?.ingestion_url && (
<Form.Item
name="ingestion_url"
label="Ingestion URL"
rules={[{ required: true, message: 'Please enter ingestion URL' }]}
>
<Input placeholder="Enter ingestion URL" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.ingestion_key && (
<Form.Item
name="ingestion_key"
label="Ingestion Key"
rules={[{ required: true, message: 'Please enter ingestion key' }]}
>
<Input placeholder="Enter ingestion key" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.signoz_api_url && (
<Form.Item
name="signoz_api_url"
label="SigNoz API URL"
rules={[{ required: true, message: 'Please enter SigNoz API URL' }]}
>
<Input placeholder="Enter SigNoz API URL" disabled={isFormDisabled} />
</Form.Item>
)}
</Form.Item>
);
}
RenderConnectionFields.defaultProps = {
connectionParams: null,
isFormDisabled: false,
isConnectionParamsLoading: false,
};
export default RenderConnectionFields;

View File

@@ -0,0 +1,156 @@
.cloud-account-setup-success-view {
display: flex;
flex-direction: column;
gap: 40px;
text-align: center;
padding-top: 34px;
p,
h3,
h4 {
margin: 0;
}
&__content {
display: flex;
flex-direction: column;
gap: 14px;
.cloud-account-setup-success-view {
&__title {
h3 {
color: var(--bg-vanilla-100);
font-size: 20px;
font-weight: 500;
line-height: 32px;
}
}
&__description {
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
}
}
&__what-next {
display: flex;
flex-direction: column;
gap: 18px;
text-align: left;
&-title {
color: var(--bg-slate-50);
font-size: 11px;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
}
.what-next-items-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
&__item {
display: flex;
gap: 10px;
align-items: baseline;
&.ant-alert {
padding: 14px;
border-radius: 8px;
font-size: 14px;
line-height: 20px; /* 142.857% */
letter-spacing: -0.21px;
}
&.ant-alert-info {
border: 1px solid rgba(63, 94, 204, 0.5);
background: rgba(78, 116, 248, 0.2);
color: var(--bg-robin-400);
}
.what-next-item {
color: var(--bg-robin-400);
&-bullet-icon {
font-size: 20px;
line-height: 20px;
}
&-text {
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.21px;
}
}
}
}
}
&__footer {
padding-top: 18px;
.ant-btn {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
height: 36px;
}
}
}
.lottie-container {
position: absolute;
width: 743.5px;
height: 990.342px;
top: -100px;
left: -36px;
z-index: 1;
}
.lightMode {
.cloud-account-setup-success-view {
&__content {
.cloud-account-setup-success-view {
&__title {
h3 {
color: var(--bg-ink-500);
}
}
&__description {
color: var(--bg-ink-400);
}
}
}
&__what-next {
&-title {
color: var(--bg-ink-500);
}
.what-next-items-wrapper {
&__item {
&.ant-alert-info {
border: 1px solid rgba(63, 94, 204, 0.2);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-500);
}
.what-next-item {
color: var(--bg-robin-500);
&-text {
color: var(--bg-robin-500);
}
}
}
}
}
&__footer {
.ant-btn {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
&:hover {
background: var(--bg-robin-400);
}
}
}
}
}

View File

@@ -0,0 +1,73 @@
import './SuccessView.style.scss';
import { Alert } from 'antd';
import integrationsSuccess from 'assets/Lotties/integrations-success.json';
import { useState } from 'react';
import Lottie from 'react-lottie';
export function SuccessView(): JSX.Element {
const [isAnimationComplete, setIsAnimationComplete] = useState(false);
const defaultOptions = {
loop: false,
autoplay: true,
animationData: integrationsSuccess,
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
},
};
return (
<>
{!isAnimationComplete && (
<div className="lottie-container">
<Lottie
options={defaultOptions}
height={990.342}
width={743.5}
eventListeners={[
{
eventName: 'complete',
callback: (): void => setIsAnimationComplete(true),
},
]}
/>
</div>
)}
<div className="cloud-account-setup-success-view">
<div className="cloud-account-setup-success-view__icon">
<img src="Icons/solid-check-circle.svg" alt="Success" />
</div>
<div className="cloud-account-setup-success-view__content">
<div className="cloud-account-setup-success-view__title">
<h3>🎉 Success! </h3>
<h3>Your AWS Web Service integration is all set.</h3>
</div>
<div className="cloud-account-setup-success-view__description">
<p>Your observability journey is off to a great start. </p>
<p>Now that your data is flowing, heres what you can do next:</p>
</div>
</div>
<div className="cloud-account-setup-success-view__what-next">
<h4 className="cloud-account-setup-success-view__what-next-title">
WHAT NEXT
</h4>
<div className="what-next-items-wrapper">
<Alert
message={
<div className="what-next-items-wrapper__item">
<div className="what-next-item-bullet-icon"></div>
<div className="what-next-item-text">
Set up your AWS services effortlessly under your enabled account.
</div>
</div>
}
type="info"
className="what-next-items-wrapper__item"
/>
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,35 @@
import { FormInstance } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import { ConnectionParams } from 'types/api/integrations/aws';
export enum ActiveViewEnum {
SELECT_REGIONS = 'select-regions',
FORM = 'form',
}
export enum ModalStateEnum {
FORM = 'form',
WAITING = 'waiting',
ERROR = 'error',
SUCCESS = 'success',
}
export interface RegionFormProps {
form: FormInstance;
modalState: ModalStateEnum;
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
selectedRegions: string[];
includeAllRegions: boolean;
onIncludeAllRegionsChange: (checked: boolean) => void;
onRegionSelect: () => void;
onSubmit: () => Promise<void>;
accountId?: string;
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
connectionParams?: ConnectionParams;
isConnectionParamsLoading?: boolean;
}
export interface IntegrationModalProps {
onClose: () => void;
}

View File

@@ -0,0 +1,50 @@
import { Link } from 'react-router-dom';
import { ServiceData } from './types';
function DashboardItem({
dashboard,
}: {
dashboard: ServiceData['assets']['dashboards'][number];
}): JSX.Element {
const content = (
<>
<div className="cloud-service-dashboard-item__title">{dashboard.title}</div>
<div className="cloud-service-dashboard-item__preview">
<img
src={dashboard.image}
alt={dashboard.title}
className="cloud-service-dashboard-item__preview-image"
/>
</div>
</>
);
return (
<div className="cloud-service-dashboard-item">
{dashboard.url ? (
<Link to={dashboard.url} className="cloud-service-dashboard-item__link">
{content}
</Link>
) : (
content
)}
</div>
);
}
function CloudServiceDashboards({
service,
}: {
service: ServiceData;
}): JSX.Element {
return (
<>
{service.assets.dashboards.map((dashboard) => (
<DashboardItem key={dashboard.id} dashboard={dashboard} />
))}
</>
);
}
export default CloudServiceDashboards;

View File

@@ -0,0 +1,87 @@
import { Table } from 'antd';
import { ServiceData } from './types';
function CloudServiceDataCollected({
logsData,
metricsData,
}: {
logsData: ServiceData['data_collected']['logs'];
metricsData: ServiceData['data_collected']['metrics'];
}): JSX.Element {
const logsColumns = [
{
title: 'NAME',
dataIndex: 'name',
key: 'name',
width: '30%',
},
{
title: 'PATH',
dataIndex: 'path',
key: 'path',
width: '40%',
},
{
title: 'FACET TYPE',
dataIndex: 'type',
key: 'type',
width: '30%',
},
];
const metricsColumns = [
{
title: 'NAME',
dataIndex: 'name',
key: 'name',
width: '40%',
},
{
title: 'UNIT',
dataIndex: 'unit',
key: 'unit',
width: '30%',
},
{
title: 'TYPE',
dataIndex: 'type',
key: 'type',
width: '30%',
},
];
const tableProps = {
pagination: { pageSize: 20, hideOnSinglePage: true },
showHeader: true,
size: 'middle' as const,
bordered: false,
};
return (
<div className="cloud-service-data-collected">
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Logs</div>
<Table
columns={logsColumns}
dataSource={logsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-logs"
/>
</div>
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Metrics</div>
<Table
columns={metricsColumns}
dataSource={metricsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-metrics"
/>
</div>
</div>
);
}
export default CloudServiceDataCollected;

View File

@@ -0,0 +1,89 @@
.configure-service-modal {
&__body {
display: flex;
flex-direction: column;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
padding: 14px;
&-regions-switch-switch {
display: flex;
align-items: center;
gap: 6px;
&-label {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
}
&-switch-description {
margin-top: 4px;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
&-form-item {
&:last-child {
margin-bottom: 0px;
}
}
}
.ant-modal-body {
padding-bottom: 0;
}
.ant-modal-footer {
margin: 0;
padding-bottom: 12px;
}
}
.lightMode {
.configure-service-modal {
&__body {
border-color: var(--bg-vanilla-300);
&-regions-switch-switch {
&-label {
color: var(--bg-ink-500);
}
}
&-switch-description {
color: var(--bg-ink-400);
}
}
.ant-btn {
&.ant-btn-default {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
&.ant-btn-primary {
// Keep primary button same as dark mode
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
&:hover {
background: var(--bg-robin-400);
}
&:disabled {
opacity: 0.6;
}
}
}
}
}

View File

@@ -0,0 +1,199 @@
import './ConfigureServiceModal.styles.scss';
import { Form, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
ServiceConfig,
SupportedSignals,
} from 'container/CloudIntegrationPage/ServicesSection/types';
import { useUpdateServiceConfig } from 'hooks/integrations/aws/useUpdateServiceConfig';
import { useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
interface IConfigureServiceModalProps {
isOpen: boolean;
onClose: () => void;
serviceName: string;
serviceId: string;
cloudAccountId: string;
supportedSignals: SupportedSignals;
initialConfig?: ServiceConfig;
}
function ConfigureServiceModal({
isOpen,
onClose,
serviceName,
serviceId,
cloudAccountId,
initialConfig,
supportedSignals,
}: IConfigureServiceModalProps): JSX.Element {
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
// Track current form values
const [currentValues, setCurrentValues] = useState({
metrics: initialConfig?.metrics?.enabled || false,
logs: initialConfig?.logs?.enabled || false,
});
const {
mutate: updateServiceConfig,
isLoading: isUpdating,
} = useUpdateServiceConfig();
const queryClient = useQueryClient();
const handleSubmit = useCallback(async (): Promise<void> => {
try {
const values = await form.validateFields();
setIsLoading(true);
updateServiceConfig(
{
serviceId,
payload: {
cloud_account_id: cloudAccountId,
config: {
logs: {
enabled: values.logs,
},
metrics: {
enabled: values.metrics,
},
},
},
},
{
onSuccess: () => {
queryClient.invalidateQueries([
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
serviceId,
]);
onClose();
},
onError: (error) => {
console.error('Failed to update service config:', error);
},
},
);
} catch (error) {
console.error('Form submission failed:', error);
} finally {
setIsLoading(false);
}
}, [
form,
updateServiceConfig,
serviceId,
cloudAccountId,
queryClient,
onClose,
]);
const isSaveDisabled = useMemo(
() => currentValues.metrics === false && currentValues.logs === false,
[currentValues],
);
const handleClose = useCallback(() => {
form.resetFields();
onClose();
}, [form, onClose]);
return (
<SignozModal
title={
<div className="account-settings-modal__title">Configure {serviceName}</div>
}
centered
open={isOpen}
okText="Save"
okButtonProps={{
disabled: isSaveDisabled,
className: 'account-settings-modal__footer-save-button',
loading: isLoading || isUpdating,
}}
onCancel={handleClose}
onOk={handleSubmit}
cancelText="Close"
cancelButtonProps={{
className: 'account-settings-modal__footer-close-button',
}}
width={672}
rootClassName=" configure-service-modal"
>
<Form
form={form}
layout="vertical"
initialValues={{
metrics: initialConfig?.metrics?.enabled || false,
logs: initialConfig?.logs?.enabled || false,
}}
>
<div className=" configure-service-modal__body">
{supportedSignals.metrics && (
<Form.Item
name="metrics"
valuePropName="checked"
className="configure-service-modal__body-form-item"
>
<div className="configure-service-modal__body-regions-switch-switch">
<Switch
checked={currentValues.metrics}
onChange={(checked): void => {
setCurrentValues((prev) => ({ ...prev, metrics: checked }));
form.setFieldsValue({ metrics: checked });
}}
/>
<span className="configure-service-modal__body-regions-switch-switch-label">
Metric Collection
</span>
</div>
<div className="configure-service-modal__body-switch-description">
Metric Collection is enabled for this AWS account. We recommend keeping
this enabled, but you can disable metric collection if you do not want
to monitor your AWS infrastructure.
</div>
</Form.Item>
)}
{supportedSignals.logs && (
<Form.Item
name="logs"
valuePropName="checked"
className="configure-service-modal__body-form-item"
>
<div className="configure-service-modal__body-regions-switch-switch">
<Switch
checked={currentValues.logs}
onChange={(checked): void => {
setCurrentValues((prev) => ({ ...prev, logs: checked }));
form.setFieldsValue({ logs: checked });
}}
/>
<span className="configure-service-modal__body-regions-switch-switch-label">
Log Collection
</span>
</div>
<div className="configure-service-modal__body-switch-description">
To ingest logs from your AWS services, you must complete several steps
</div>
</Form.Item>
)}
</div>
</Form>
</SignozModal>
);
}
ConfigureServiceModal.defaultProps = {
initialConfig: {
metrics: { enabled: false },
logs: { enabled: false },
},
};
export default ConfigureServiceModal;

View File

@@ -0,0 +1,158 @@
import { Button, Tabs, TabsProps } from 'antd';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import Spinner from 'components/Spinner';
import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDashboards';
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
import dayjs from 'dayjs';
import { useServiceDetails } from 'hooks/integrations/aws/useServiceDetails';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo, useState } from 'react';
import ConfigureServiceModal from './ConfigureServiceModal';
const getStatus = (
logsLastReceivedTimestamp: number | undefined,
metricsLastReceivedTimestamp: number | undefined,
): { text: string; className: string } => {
if (!logsLastReceivedTimestamp && !metricsLastReceivedTimestamp) {
return { text: 'No Data Yet', className: 'service-status--no-data' };
}
const latestTimestamp = Math.max(
logsLastReceivedTimestamp || 0,
metricsLastReceivedTimestamp || 0,
);
const isStale = dayjs().diff(dayjs(latestTimestamp), 'minute') > 30;
if (isStale) {
return { text: 'Stale Data', className: 'service-status--stale-data' };
}
return { text: 'Connected', className: 'service-status--connected' };
};
function ServiceStatus({
serviceStatus,
}: {
serviceStatus: IServiceStatus | undefined;
}): JSX.Element {
const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms;
const metricsLastReceivedTimestamp =
serviceStatus?.metrics?.last_received_ts_ms;
const { text, className } = getStatus(
logsLastReceivedTimestamp,
metricsLastReceivedTimestamp,
);
return <div className={`service-status ${className}`}>{text}</div>;
}
function ServiceDetails(): JSX.Element | null {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId');
const serviceId = urlQuery.get('service');
const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState(
false,
);
const { data: serviceDetailsData, isLoading } = useServiceDetails(
serviceId || '',
cloudAccountId || undefined,
);
// eslint-disable-next-line @typescript-eslint/naming-convention
const { config, supported_signals } = serviceDetailsData ?? {};
const totalSupportedSignals = Object.entries(supported_signals || {}).filter(
([, value]) => !!value,
).length;
const enabledSignals = useMemo(
() =>
Object.values(config || {}).filter((item) => item && item.enabled).length,
[config],
);
const isAnySignalConfigured = useMemo(
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
[config],
);
if (isLoading) {
return <Spinner size="large" height="50vh" />;
}
if (!serviceDetailsData) {
return null;
}
const tabItems: TabsProps['items'] = [
{
key: 'dashboards',
label: `Dashboards (${serviceDetailsData?.assets.dashboards.length})`,
children: <CloudServiceDashboards service={serviceDetailsData} />,
},
{
key: 'data-collected',
label: 'Data Collected',
children: (
<CloudServiceDataCollected
logsData={serviceDetailsData?.data_collected.logs || []}
metricsData={serviceDetailsData?.data_collected.metrics || []}
/>
),
},
];
return (
<div className="service-details">
<div className="service-details__title-bar">
<div className="service-details__details-title">Details</div>
<div className="service-details__right-actions">
<ServiceStatus serviceStatus={serviceDetailsData.status} />
{!!cloudAccountId && isAnySignalConfigured ? (
<Button
className="configure-button configure-button--default"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Configure ({enabledSignals}/{totalSupportedSignals})
</Button>
) : (
<Button
type="primary"
className="configure-button configure-button--primary"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Enable Service
</Button>
)}
</div>
</div>
<div className="service-details__overview">
<MarkdownRenderer
variables={{}}
markdownContent={serviceDetailsData?.overview}
/>
</div>
<div className="service-details__tabs">
<Tabs items={tabItems} />
</div>
{isConfigureServiceModalOpen && (
<ConfigureServiceModal
isOpen
onClose={(): void => setIsConfigureServiceModalOpen(false)}
serviceName={serviceDetailsData.title}
serviceId={serviceId || ''}
cloudAccountId={cloudAccountId || ''}
initialConfig={serviceDetailsData.config}
supportedSignals={serviceDetailsData.supported_signals || {}}
/>
)}
</div>
);
}
export default ServiceDetails;

View File

@@ -0,0 +1,39 @@
import cx from 'classnames';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { Service } from './types';
function ServiceItem({
service,
onClick,
isActive,
}: {
service: Service;
onClick: (serviceName: string) => void;
isActive?: boolean;
}): JSX.Element {
return (
<button
className={cx('service-item', { active: isActive })}
onClick={(): void => onClick(service.id)}
type="button"
>
<div className="service-item__icon-wrapper">
<img
src={service.icon}
alt={service.title}
className="service-item__icon"
/>
</div>
<div className="service-item__title">
<LineClampedText text={service.title} />
</div>
</button>
);
}
ServiceItem.defaultProps = {
isActive: false,
};
export default ServiceItem;

View File

@@ -0,0 +1,66 @@
import Spinner from 'components/Spinner';
import { useGetAccountServices } from 'hooks/integrations/aws/useGetAccountServices';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import ServiceItem from './ServiceItem';
interface ServicesListProps {
cloudAccountId: string;
filter: 'all_services' | 'enabled' | 'available';
}
function ServicesList({
cloudAccountId,
filter,
}: ServicesListProps): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: services = [], isLoading } = useGetAccountServices(
cloudAccountId,
);
const activeService = urlQuery.get('service');
const handleActiveService = useCallback(
(serviceId: string): void => {
urlQuery.set('service', serviceId);
navigate({ search: urlQuery.toString() });
},
[navigate, urlQuery],
);
const filteredServices = useMemo(() => {
if (filter === 'all_services') return services;
return services.filter((service) => {
const isEnabled =
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
return filter === 'enabled' ? isEnabled : !isEnabled;
});
}, [services, filter]);
useEffect(() => {
if (activeService || !services?.length) return;
handleActiveService(services[0].id);
}, [services, activeService, handleActiveService]);
if (isLoading) return <Spinner size="large" height="25vh" />;
if (!services) return <div>No services found</div>;
return (
<div className="services-list">
{filteredServices.map((service) => (
<ServiceItem
key={service.id}
service={service}
onClick={handleActiveService}
isActive={service.id === activeService}
/>
))}
</div>
);
}
export default ServicesList;

View File

@@ -0,0 +1,438 @@
.services-tabs {
.ant-tabs-tab {
font-family: 'Inter';
padding: 16px 4px 14px;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
font-weight: 400;
&.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: var(--bg-vanilla-100);
}
}
}
.ant-tabs-ink-bar {
background: var(--bg-robin-500);
}
}
.services-section {
display: flex;
gap: 10px;
&__sidebar {
width: 16%;
padding: 0 16px;
}
&__content {
width: 84%;
padding: 16px;
}
}
.services-filter {
padding: 16px 0;
.ant-select-selector {
background-color: var(--bg-ink-300) !important;
border: 1px solid var(--bg-slate-400) !important;
color: var(--bg-vanilla-400) !important;
font-family: Inter;
font-size: 12px;
}
.ant-select-arrow {
color: var(--bg-vanilla-400);
}
}
.service-item {
display: flex;
gap: 12px;
align-items: center;
padding: 8px;
background-color: transparent;
border: none;
width: 100%;
cursor: pointer;
&:not(:last-child) {
border-bottom: 1px solid var(--bg-slate-400);
}
&.active {
background-color: var(--bg-ink-100);
}
&__icon-wrapper {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
border-radius: 4px;
.service-item__icon {
width: 24px;
height: 24px;
}
}
&__title {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
text-align: left;
}
}
.service-details {
display: flex;
flex-direction: column;
gap: 10px;
&__title-bar {
display: flex;
justify-content: space-between;
align-items: center;
height: 48px;
border-bottom: 1px solid var(--bg-slate-400);
.service-details__details-title {
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
text-align: left;
}
.service-details__right-actions {
display: flex;
align-items: center;
gap: 8px;
.service-status {
display: flex;
align-items: center;
padding: 2px 8px;
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 500;
letter-spacing: 0.54px;
border-radius: 2px;
line-height: normal;
&--connected {
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-400);
}
&--stale-data {
background: rgba(255, 215, 120, 0.1);
border: 1px solid rgba(255, 215, 120, 0.1);
color: var(--bg-amber-400);
}
&--no-data {
border: 1px solid rgba(234, 109, 113, 0.1);
background: rgba(234, 109, 113, 0.1);
color: var(--bg-cherry-400);
}
}
.configure-button {
border-radius: 2px;
font-size: 12px;
letter-spacing: 0.12px;
font-weight: 500;
width: 116px;
box-shadow: none;
&--default {
color: var(--bg-vanilla-400);
background: var(--bg-slate-400);
border: 1px solid var(--bg-slate-400);
}
&--primary {
background-color: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-weight: 500;
color: var(--Vanilla-100, #fff);
}
}
}
}
&__overview {
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 800px;
}
&__tabs {
.ant-tabs {
&-ink-bar {
background-color: transparent;
}
&-nav {
padding: 8px 0 18px;
&-wrap {
padding: 0;
}
&::before {
border-bottom: none !important;
}
}
&-tab {
margin-left: 0 !important;
padding: 0;
&-btn {
padding: 10px 24px;
color: var(--text-vanilla-400) !important;
font-family: Inter;
font-size: 12px;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
&[aria-selected='true'] {
color: var(--text-vanilla-100) !important;
}
}
&-active {
background: var(--bg-slate-400, #1d212d);
}
}
&-extra-content {
padding: 0 16px 16px;
}
&-nav-list {
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
border-radius: 2px;
}
}
}
.cloud-service {
&-dashboard-item {
display: flex;
flex-direction: column;
gap: 10px;
&__title {
color: var(--bg-vanilla-100);
font-size: 18px;
font-weight: 500;
line-height: 20px; /* 111.111% */
letter-spacing: -0.09px;
}
&__preview-image {
width: 100%;
}
}
&-data-collected {
display: flex;
flex-direction: column;
gap: 24px;
&__table {
display: flex;
flex-direction: column;
gap: 8px;
.ant-table {
background: transparent;
border: 1px solid var(--bg-slate-400);
.ant-table-thead {
> tr > th {
&::before {
display: none;
}
background: transparent;
border: none;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 600;
line-height: 18px; /* 150% */
letter-spacing: 0.6px;
text-transform: uppercase;
}
}
.ant-table-tbody {
> tr {
&:nth-child(odd),
&:hover > td {
background: rgba(255, 255, 255, 0.01) !important;
}
> td {
&:first-child {
font-weight: 500;
}
border: none;
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
}
&__table-heading {
color: var(--bg-slate-50);
font-size: 11px;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
}
}
}
.lightMode {
.services-tabs {
.ant-tabs-tab {
&.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: var(--bg-ink-500);
}
}
}
}
.services-filter {
.ant-select-selector {
background-color: var(--bg-vanilla-100) !important;
border-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-400) !important;
}
.ant-select-arrow {
color: var(--bg-ink-400);
}
}
.service-item {
&:not(:last-child) {
border-bottom: 1px solid var(--bg-vanilla-300);
}
&.active {
background-color: var(--bg-vanilla-300);
}
&__icon-wrapper {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
&__title {
color: var(--bg-ink-500);
}
}
.service-details {
&__title-bar {
border-bottom: 1px solid var(--bg-vanilla-300);
.service-details__details-title {
color: var(--bg-ink-400);
}
.configure-button {
color: var(--bg-ink-400);
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
.service-status {
&--connected {
border: 1px solid rgba(37, 225, 146, 0.2);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-500);
}
&--stale-data {
border: 1px solid rgba(255, 215, 120, 0.2);
background: rgba(255, 215, 120, 0.1);
color: var(--bg-amber-500);
}
&--no-data {
border: 1px solid rgba(234, 109, 113, 0.2);
background: rgba(234, 109, 113, 0.1);
color: var(--bg-cherry-500);
}
}
}
&__overview {
color: var(--bg-ink-400);
}
&__tabs {
.ant-tabs {
&-tab {
&-btn {
color: var(--bg-ink-400) !important;
&[aria-selected='true'] {
color: var(--bg-ink-500) !important;
}
}
&-active {
background: var(--bg-vanilla-300);
}
}
&-nav-list {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
.cloud-service {
&-dashboard-item {
&__title {
color: var(--bg-ink-500);
}
}
&-data-collected {
&__table {
.ant-table {
border-color: var(--bg-vanilla-300);
.ant-table-thead {
> tr > th {
color: var(--bg-ink-400);
}
}
.ant-table-tbody {
> tr {
&:nth-child(odd),
&:hover > td {
background: var(--bg-vanilla-100) !important;
}
> td {
color: var(--bg-ink-400);
}
}
}
}
}
&__table-heading {
color: var(--bg-ink-500);
}
}
}
}
}

View File

@@ -0,0 +1,118 @@
import './ServicesTabs.style.scss';
import { Color } from '@signozhq/design-tokens';
import type { SelectProps, TabsProps } from 'antd';
import { Select, Tabs } from 'antd';
import { getAwsServices } from 'api/integrations/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useUrlQuery from 'hooks/useUrlQuery';
import { ChevronDown } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import ServiceDetails from './ServiceDetails';
import ServicesList from './ServicesList';
export enum ServiceFilterType {
ALL_SERVICES = 'all_services',
ENABLED = 'enabled',
AVAILABLE = 'available',
}
interface ServicesFilterProps {
cloudAccountId: string;
onFilterChange: (value: ServiceFilterType) => void;
}
function ServicesFilter({
cloudAccountId,
onFilterChange,
}: ServicesFilterProps): JSX.Element | null {
const { data: services, isLoading } = useQuery(
[REACT_QUERY_KEY.AWS_SERVICES, cloudAccountId],
() => getAwsServices(cloudAccountId),
);
const { enabledCount, availableCount } = useMemo(() => {
if (!services) return { enabledCount: 0, availableCount: 0 };
return services.reduce(
(acc, service) => {
const isEnabled =
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
return {
enabledCount: acc.enabledCount + (isEnabled ? 1 : 0),
availableCount: acc.availableCount + (isEnabled ? 0 : 1),
};
},
{ enabledCount: 0, availableCount: 0 },
);
}, [services]);
const selectOptions: SelectProps['options'] = useMemo(
() => [
{ value: 'all_services', label: `All Services (${services?.length || 0})` },
{ value: 'enabled', label: `Enabled (${enabledCount})` },
{ value: 'available', label: `Available (${availableCount})` },
],
[services, enabledCount, availableCount],
);
if (isLoading) return null;
if (!services?.length) return null;
return (
<div className="services-filter">
<Select
style={{ width: '100%' }}
defaultValue={ServiceFilterType.ALL_SERVICES}
options={selectOptions}
className="services-sidebar__select"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
onChange={onFilterChange}
/>
</div>
);
}
function ServicesSection(): JSX.Element {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
const [activeFilter, setActiveFilter] = useState<
'all_services' | 'enabled' | 'available'
>('all_services');
return (
<div className="services-section">
<div className="services-section__sidebar">
<ServicesFilter
cloudAccountId={cloudAccountId}
onFilterChange={setActiveFilter}
/>
<ServicesList cloudAccountId={cloudAccountId} filter={activeFilter} />
</div>
<div className="services-section__content">
<ServiceDetails />
</div>
</div>
);
}
function ServicesTabs(): JSX.Element {
const tabItems: TabsProps['items'] = [
{
key: 'services',
label: 'Services For Integration',
children: <ServicesSection />,
},
];
return (
<div className="services-tabs">
<Tabs defaultActiveKey="services" items={tabItems} />
</div>
);
}
export default ServicesTabs;

View File

@@ -0,0 +1,135 @@
interface Service {
id: string;
title: string;
icon: string;
config: ServiceConfig;
}
interface Dashboard {
id: string;
url: string;
title: string;
description: string;
image: string;
}
interface LogField {
name: string;
path: string;
type: string;
}
interface Metric {
name: string;
type: string;
unit: string;
}
interface ConfigStatus {
enabled: boolean;
}
interface DataStatus {
last_received_ts_ms: number;
last_received_from: string;
}
interface ServiceConfig {
logs: ConfigStatus;
metrics: ConfigStatus;
}
interface IServiceStatus {
logs: DataStatus | null;
metrics: DataStatus | null;
}
interface SupportedSignals {
metrics: boolean;
logs: boolean;
}
interface ServiceData {
id: string;
title: string;
icon: string;
overview: string;
supported_signals: SupportedSignals;
assets: {
dashboards: Dashboard[];
};
data_collected: {
logs?: LogField[];
metrics: Metric[];
};
config?: ServiceConfig;
status?: IServiceStatus;
}
interface ServiceDetailsResponse {
status: 'success';
data: ServiceData;
}
interface CloudAccountConfig {
regions: string[];
}
interface IntegrationStatus {
last_heartbeat_ts_ms: number;
}
interface AccountStatus {
integration: IntegrationStatus;
}
interface CloudAccount {
id: string;
cloud_account_id: string;
config: CloudAccountConfig;
status: AccountStatus;
}
interface CloudAccountsData {
accounts: CloudAccount[];
}
interface UpdateServiceConfigPayload {
cloud_account_id: string;
config: {
logs: {
enabled: boolean;
};
metrics: {
enabled: boolean;
};
};
}
interface UpdateServiceConfigResponse {
status: string;
data: {
id: string;
config: {
logs: {
enabled: boolean;
};
metrics: {
enabled: boolean;
};
};
};
}
export type {
CloudAccount,
CloudAccountsData,
IServiceStatus,
Service,
ServiceConfig,
ServiceData,
ServiceDetailsResponse,
SupportedSignals,
UpdateServiceConfigPayload,
UpdateServiceConfigResponse,
};

View File

@@ -25,7 +25,10 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
import { useOptionsMenu } from 'container/OptionsMenu';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import {
defaultLogsSelectedColumns,
defaultTraceSelectedColumns,
} from 'container/OptionsMenu/constants';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -408,6 +411,14 @@ function ExplorerOptions({
const handleClearSelect = (): void => {
removeCurrentViewFromLocalStorage();
handleOptionsChange({
...options,
selectColumns:
sourcepage === DataSource.TRACES
? defaultTraceSelectedColumns
: defaultLogsSelectedColumns,
});
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
};

Some files were not shown because too many files have changed in this diff Show More