Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d17dab9a1d | ||
|
|
88b75d4e72 | ||
|
|
6327ab5ec6 | ||
|
|
5b09490ad7 | ||
|
|
b50127b567 | ||
|
|
ba2ed3ad22 | ||
|
|
eb3dfbf63b | ||
|
|
c3e048470d | ||
|
|
4563ff0e62 | ||
|
|
c9e48b6de9 | ||
|
|
06ef9ff384 | ||
|
|
26d55875f5 | ||
|
|
b1864ee328 | ||
|
|
8b62c8dced | ||
|
|
273452352d | ||
|
|
8274ebfe37 | ||
|
|
7d5e14abb6 | ||
|
|
7c17ac42b1 | ||
|
|
74ee7bb2c7 | ||
|
|
2f5640b2e6 | ||
|
|
121debcecc | ||
|
|
ff13504a74 | ||
|
|
d4e373443b | ||
|
|
3ccf822d67 | ||
|
|
0e270e6f51 | ||
|
|
749df2a979 | ||
|
|
9ee5d5d599 | ||
|
|
4940dfd46f | ||
|
|
79a31cc205 | ||
|
|
5102cf2b7b | ||
|
|
9ec5594648 | ||
|
|
b6c2ebd6d7 | ||
|
|
9a3a8c8305 | ||
|
|
2ac45b0174 | ||
|
|
2a53918ebd | ||
|
|
9daefeb881 | ||
|
|
526cf01cb7 | ||
|
|
cd4766ec2b | ||
|
|
2196b58d36 | ||
|
|
53c58b9983 | ||
|
|
d174038dce | ||
|
|
78d09e2940 | ||
|
|
6cb7f152e1 | ||
|
|
f6730d3d09 | ||
|
|
899a6ab70a | ||
|
|
a4b852bb99 | ||
|
|
92cd108c0d | ||
|
|
34c116fc7e | ||
|
|
250646a354 | ||
|
|
00191d5774 | ||
|
|
525a0d7a1a | ||
|
|
564edc7430 | ||
|
|
78f396b94a | ||
|
|
9e53c150b8 | ||
|
|
f80a6c3014 | ||
|
|
1eff6d82c9 | ||
|
|
f138eff26c | ||
|
|
50f3fc0ff9 | ||
|
|
ebcb172614 | ||
|
|
133c0deaa8 | ||
|
|
35e8165463 | ||
|
|
6d009c6607 | ||
|
|
f0994e52c0 | ||
|
|
7f5b388722 | ||
|
|
b11a4c0c21 | ||
|
|
bbb21f608f | ||
|
|
50a5b88708 | ||
|
|
5601c0886d | ||
|
|
5b342b9b5d | ||
|
|
7ec59c3c77 | ||
|
|
a12990f0bd | ||
|
|
1ee1ca7951 | ||
|
|
3b1bf34d3e | ||
|
|
fbcff29fae | ||
|
|
81fcca3bd3 | ||
|
|
4f7d84aa37 | ||
|
|
8f8dedb8b3 | ||
|
|
3f65229506 | ||
|
|
f006260719 | ||
|
|
3fc8f6c353 | ||
|
|
e02ae9a5c4 | ||
|
|
1989d07e52 | ||
|
|
78194ae955 | ||
|
|
da1b6d1ed0 | ||
|
|
d3c76ae8be | ||
|
|
bed3dbc698 | ||
|
|
66affb0ece | ||
|
|
75f62372ae | ||
|
|
a3ac307b4e | ||
|
|
7672d2f636 | ||
|
|
e3018d9529 | ||
|
|
385ee268e3 | ||
|
|
01036a8a2f | ||
|
|
1542b9d6e9 | ||
|
|
8455349459 | ||
|
|
c488a24d09 | ||
|
|
9091cf61fd | ||
|
|
eeb2ab3212 | ||
|
|
3f128f0f1d | ||
|
|
59ff7ed1e1 | ||
|
|
d236b6ce1e | ||
|
|
44b118a212 | ||
|
|
3fc6f7ee63 | ||
|
|
f1016baf03 | ||
|
|
e5c0d9e44a | ||
|
|
e51056c804 | ||
|
|
7d8dad4550 | ||
|
|
c477e0ef16 | ||
|
|
fff7f8fc76 | ||
|
|
8cfeef4521 | ||
|
|
d85a1a21ac | ||
|
|
17f48d656d | ||
|
|
2d6774da68 | ||
|
|
62a9d7e602 | ||
|
|
3a2c7a7a68 | ||
|
|
33e70d1f37 | ||
|
|
85f04e4bae | ||
|
|
53f9e7d811 | ||
|
|
ad46e22561 | ||
|
|
e79195ccf1 | ||
|
|
f77bb888a8 | ||
|
|
baa15baea9 | ||
|
|
316e6821f1 | ||
|
|
a1fa2769e4 | ||
|
|
decb660992 | ||
|
|
0acbcf8322 | ||
|
|
11eabdc2ac | ||
|
|
eb94554f5a | ||
|
|
e8280dbea4 | ||
|
|
44ea237039 | ||
|
|
72b0214d1d | ||
|
|
386a215324 | ||
|
|
ba0ba4bbc9 | ||
|
|
d60c9ab36b | ||
|
|
90770b90bd | ||
|
|
a19874c1dd | ||
|
|
65ff460d63 |
@@ -40,7 +40,7 @@ services:
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
schema-migrator-sync:
|
||||
image: signoz/signoz-schema-migrator:v0.111.42
|
||||
image: signoz/signoz-schema-migrator:v0.128.0
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
condition: service_healthy
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
image: signoz/signoz-schema-migrator:v0.111.42
|
||||
image: signoz/signoz-schema-migrator:v0.128.0
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -12,4 +12,9 @@
|
||||
/pkg/factory/ @grandwizard28
|
||||
/pkg/types/ @grandwizard28
|
||||
.golangci.yml @grandwizard28
|
||||
**/(zeus|licensing|sqlmigration)/ @vikrantgupta25
|
||||
/pkg/zeus/ @vikrantgupta25
|
||||
/pkg/licensing/ @vikrantgupta25
|
||||
/pkg/sqlmigration/ @vikrantgupta25
|
||||
/ee/zeus/ @vikrantgupta25
|
||||
/ee/licensing/ @vikrantgupta25
|
||||
/ee/sqlmigration/ @vikrantgupta25
|
||||
3
.github/workflows/build-community.yaml
vendored
3
.github/workflows/build-community.yaml
vendored
@@ -74,7 +74,8 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/pkg/version.variant=community
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}'
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
|
||||
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch
|
||||
|
||||
3
.github/workflows/build-enterprise.yaml
vendored
3
.github/workflows/build-enterprise.yaml
vendored
@@ -108,7 +108,8 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1'
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
|
||||
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
|
||||
|
||||
3
.github/workflows/build-staging.yaml
vendored
3
.github/workflows/build-staging.yaml
vendored
@@ -107,7 +107,8 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1'
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1
|
||||
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
|
||||
|
||||
2
.github/workflows/integrationci.yaml
vendored
2
.github/workflows/integrationci.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- 24.1.2-alpine
|
||||
- 24.12-alpine
|
||||
schema-migrator-version:
|
||||
- v0.111.38
|
||||
- v0.128.0
|
||||
postgres-version:
|
||||
- 15
|
||||
if: |
|
||||
|
||||
@@ -7,6 +7,7 @@ linters:
|
||||
- sloglint
|
||||
- depguard
|
||||
- iface
|
||||
- unparam
|
||||
|
||||
linters-settings:
|
||||
sloglint:
|
||||
|
||||
@@ -90,6 +90,15 @@ apiserver:
|
||||
- /api/v1/version
|
||||
- /
|
||||
|
||||
##################### Querier #####################
|
||||
querier:
|
||||
# The TTL for cached query results.
|
||||
cache_ttl: 168h
|
||||
# The interval for recent data that should not be cached.
|
||||
flux_interval: 5m
|
||||
# The maximum number of concurrent queries for missing ranges.
|
||||
max_concurrent_queries: 4
|
||||
|
||||
##################### TelemetryStore #####################
|
||||
telemetrystore:
|
||||
# Maximum number of idle connections in the connection pool.
|
||||
@@ -103,13 +112,15 @@ telemetrystore:
|
||||
clickhouse:
|
||||
# The DSN to use for clickhouse.
|
||||
dsn: tcp://localhost:9000
|
||||
# The cluster name to use for clickhouse.
|
||||
cluster: cluster
|
||||
# The query settings for clickhouse.
|
||||
settings:
|
||||
max_execution_time: 0
|
||||
max_execution_time_leaf: 0
|
||||
timeout_before_checking_execution_speed: 0
|
||||
max_bytes_to_read: 0
|
||||
max_result_rows_for_ch_query: 0
|
||||
max_result_rows: 0
|
||||
|
||||
##################### Prometheus #####################
|
||||
prometheus:
|
||||
@@ -165,12 +176,6 @@ alertmanager:
|
||||
# Retention of the notification logs.
|
||||
retention: 120h
|
||||
|
||||
|
||||
##################### Analytics #####################
|
||||
analytics:
|
||||
# Whether to enable analytics.
|
||||
enabled: false
|
||||
|
||||
##################### Emailing #####################
|
||||
emailing:
|
||||
# Whether to enable emailing.
|
||||
@@ -215,3 +220,27 @@ sharder:
|
||||
single:
|
||||
# The org id to which this instance belongs to.
|
||||
org_id: org_id
|
||||
|
||||
##################### Analytics #####################
|
||||
analytics:
|
||||
# Whether to enable analytics.
|
||||
enabled: false
|
||||
segment:
|
||||
# The key to use for segment.
|
||||
key: ""
|
||||
|
||||
##################### StatsReporter #####################
|
||||
statsreporter:
|
||||
# Whether to enable stats reporter. This is used to provide valuable insights to the SigNoz team. It does not collect any sensitive/PII data.
|
||||
enabled: true
|
||||
# The interval at which the stats are collected.
|
||||
interval: 6h
|
||||
collect:
|
||||
# Whether to collect identities and traits (emails).
|
||||
identities: true
|
||||
|
||||
|
||||
##################### Gateway (License only) #####################
|
||||
gateway:
|
||||
# The URL of the gateway's api.
|
||||
url: http://localhost:8080
|
||||
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.86.2
|
||||
image: signoz/signoz:v0.89.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
@@ -194,6 +194,7 @@ services:
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-swarm
|
||||
- SIGNOZ_JWT_SECRET=secret
|
||||
- DOT_METRICS_ENABLED=true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -206,7 +207,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.111.42
|
||||
image: signoz/signoz-otel-collector:v0.128.0
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -230,7 +231,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.111.42
|
||||
image: signoz/signoz-schema-migrator:v0.128.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -100,26 +100,32 @@ services:
|
||||
# - "9000:9000"
|
||||
# - "8123:8123"
|
||||
# - "9181:9181"
|
||||
|
||||
configs:
|
||||
- source: clickhouse-config
|
||||
target: /etc/clickhouse-server/config.xml
|
||||
- source: clickhouse-users
|
||||
target: /etc/clickhouse-server/users.xml
|
||||
- source: clickhouse-custom-function
|
||||
target: /etc/clickhouse-server/custom-function.xml
|
||||
- source: clickhouse-cluster
|
||||
target: /etc/clickhouse-server/config.d/cluster.xml
|
||||
volumes:
|
||||
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
|
||||
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
|
||||
- ../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:/var/lib/clickhouse/
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.86.2
|
||||
image: signoz/signoz:v0.89.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
volumes:
|
||||
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
|
||||
- ../common/dashboards:/root/config/dashboards
|
||||
- sqlite:/var/lib/signoz/
|
||||
configs:
|
||||
- source: signoz-prometheus-config
|
||||
target: /root/config/prometheus.yml
|
||||
environment:
|
||||
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
|
||||
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
@@ -129,6 +135,7 @@ services:
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-swarm
|
||||
- DOT_METRICS_ENABLED=true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -141,15 +148,17 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.111.42
|
||||
image: signoz/signoz-otel-collector:v0.128.0
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
configs:
|
||||
- source: otel-collector-config
|
||||
target: /etc/otel-collector-config.yaml
|
||||
- source: otel-manager-config
|
||||
target: /etc/manager-config.yaml
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
@@ -165,7 +174,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.111.42
|
||||
image: signoz/signoz-schema-migrator:v0.128.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -186,3 +195,24 @@ volumes:
|
||||
name: signoz-sqlite
|
||||
zookeeper-1:
|
||||
name: signoz-zookeeper-1
|
||||
configs:
|
||||
clickhouse-config:
|
||||
file: ../common/clickhouse/config.xml
|
||||
clickhouse-users:
|
||||
file: ../common/clickhouse/users.xml
|
||||
clickhouse-custom-function:
|
||||
file: ../common/clickhouse/custom-function.xml
|
||||
clickhouse-cluster:
|
||||
file: ../common/clickhouse/cluster.xml
|
||||
signoz-prometheus-config:
|
||||
file: ../common/signoz/prometheus.yml
|
||||
# If you have multiple dashboard files, you can list them individually:
|
||||
# dashboard-foo:
|
||||
# file: ../common/dashboards/foo.json
|
||||
# dashboard-bar:
|
||||
# file: ../common/dashboards/bar.json
|
||||
|
||||
otel-collector-config:
|
||||
file: ./otel-collector-config.yaml
|
||||
otel-manager-config:
|
||||
file: ../common/signoz/otel-collector-opamp-config.yaml
|
||||
|
||||
@@ -26,7 +26,7 @@ processors:
|
||||
detectors: [env, system]
|
||||
timeout: 2s
|
||||
signozspanmetrics/delta:
|
||||
metrics_exporter: clickhousemetricswrite, signozclickhousemetrics
|
||||
metrics_exporter: signozclickhousemetrics
|
||||
metrics_flush_interval: 60s
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
dimensions_cache_size: 100000
|
||||
@@ -60,27 +60,16 @@ exporters:
|
||||
datasource: tcp://clickhouse:9000/signoz_traces
|
||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
use_new_schema: true
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
disable_v2: true
|
||||
clickhousemetricswrite/prometheus:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
disable_v2: true
|
||||
signozclickhousemetrics:
|
||||
dsn: tcp://clickhouse:9000/signoz_metrics
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/signoz_logs
|
||||
timeout: 10s
|
||||
use_new_schema: true
|
||||
# debug: {}
|
||||
service:
|
||||
telemetry:
|
||||
logs:
|
||||
encoding: json
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- pprof
|
||||
@@ -92,11 +81,11 @@ service:
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite, signozclickhousemetrics]
|
||||
exporters: [signozclickhousemetrics]
|
||||
metrics/prometheus:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
|
||||
exporters: [signozclickhousemetrics]
|
||||
logs:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
|
||||
@@ -177,7 +177,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.86.2}
|
||||
image: signoz/signoz:${VERSION:-v0.89.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -197,6 +197,7 @@ services:
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- DOT_METRICS_ENABLED=true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -210,7 +211,7 @@ services:
|
||||
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -236,7 +237,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -247,7 +248,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.86.2}
|
||||
image: signoz/signoz:${VERSION:-v0.89.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -130,6 +130,7 @@ services:
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- DOT_METRICS_ENABLED=true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -142,7 +143,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -164,7 +165,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -176,7 +177,7 @@ services:
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -26,7 +26,7 @@ processors:
|
||||
detectors: [env, system]
|
||||
timeout: 2s
|
||||
signozspanmetrics/delta:
|
||||
metrics_exporter: clickhousemetricswrite, signozclickhousemetrics
|
||||
metrics_exporter: signozclickhousemetrics
|
||||
metrics_flush_interval: 60s
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
dimensions_cache_size: 100000
|
||||
@@ -60,27 +60,16 @@ exporters:
|
||||
datasource: tcp://clickhouse:9000/signoz_traces
|
||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
use_new_schema: true
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
disable_v2: true
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
clickhousemetricswrite/prometheus:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
disable_v2: true
|
||||
signozclickhousemetrics:
|
||||
dsn: tcp://clickhouse:9000/signoz_metrics
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/signoz_logs
|
||||
timeout: 10s
|
||||
use_new_schema: true
|
||||
# debug: {}
|
||||
service:
|
||||
telemetry:
|
||||
logs:
|
||||
encoding: json
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- pprof
|
||||
@@ -92,11 +81,11 @@ service:
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite, signozclickhousemetrics]
|
||||
exporters: [signozclickhousemetrics]
|
||||
metrics/prometheus:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
|
||||
exporters: [signozclickhousemetrics]
|
||||
logs:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
|
||||
51
docs/contributing/go/endpoint.md
Normal file
51
docs/contributing/go/endpoint.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Endpoint
|
||||
|
||||
This guide outlines the recommended approach for designing endpoints, with a focus on entity relationships, RESTful structure, and examples from the codebase.
|
||||
|
||||
## How do we design an endpoint?
|
||||
|
||||
### Understand the core entities and their relationships
|
||||
|
||||
Start with understanding the core entities and their relationships. For example:
|
||||
|
||||
- **Organization**: an organization can have multiple users
|
||||
|
||||
### Structure Endpoints RESTfully
|
||||
|
||||
Endpoints should reflect the resource hierarchy and follow RESTful conventions. Use clear, **pluralized resource names** and versioning. For example:
|
||||
|
||||
- `POST /v1/organizations` — Create an organization
|
||||
- `GET /v1/organizations/:id` — Get an organization by id
|
||||
- `DELETE /v1/organizations/:id` — Delete an organization by id
|
||||
- `PUT /v1/organizations/:id` — Update an organization by id
|
||||
- `GET /v1/organizations/:id/users` — Get all users in an organization
|
||||
- `GET /v1/organizations/me/users` — Get all users in my organization
|
||||
|
||||
Think in terms of resource navigation in a file system. For example, to find your organization, you would navigate to the root of the file system and then to the `organizations` directory. To find a user in an organization, you would navigate to the `organizations` directory and then to the `id` directory.
|
||||
|
||||
```bash
|
||||
v1/
|
||||
├── organizations/
|
||||
│ └── 123/
|
||||
│ └── users/
|
||||
```
|
||||
|
||||
`me` endpoints are special. They are used to determine the actual id via some auth/external mechanism. For `me` endpoints, think of the `me` directory being symlinked to your organization directory. For example, if you are a part of the organization `123`, the `me` directory will be symlinked to `/v1/organizations/123`:
|
||||
|
||||
```bash
|
||||
v1/
|
||||
├── organizations/
|
||||
│ └── me/ -> symlink to /v1/organizations/123
|
||||
│ └── users/
|
||||
│ └── 123/
|
||||
│ └── users/
|
||||
```
|
||||
|
||||
> 💡 **Note**: There are various ways to structure endpoints. Some prefer to use singular resource names instead of `me`. Others prefer to use singular resource names for all endpoints. We have, however, chosen to standardize our endpoints in the manner described above.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Use clear, **plural resource names**
|
||||
- Use `me` endpoints for determining the actual id via some auth mechanism
|
||||
|
||||
> 💡 **Note**: When in doubt, diagram the relationships and walk through the user flows as if navigating a file system. This will help you design endpoints that are both logical and user-friendly.
|
||||
106
docs/contributing/go/provider.md
Normal file
106
docs/contributing/go/provider.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Provider
|
||||
|
||||
SigNoz is built on the provider pattern, a design approach where code is organized into providers that handle specific application responsibilities. Providers act as adapter components that integrate with external services and deliver required functionality to the application.
|
||||
|
||||
> 💡 **Note**: Coming from a DDD background? Providers are similar (not exactly the same) to adapter/infrastructure services.
|
||||
|
||||
## How to create a new provider?
|
||||
|
||||
To create a new provider, create a directory in the `pkg/` directory named after your provider. The provider package consists of four key components:
|
||||
|
||||
- **Interface** (`pkg/<name>/<name>.go`): Defines the provider's interface. Other packages should import this interface to use the provider.
|
||||
- **Config** (`pkg/<name>/config.go`): Contains provider configuration, implementing the `factory.Config` interface from [factory/config.go](/pkg/factory/config.go).
|
||||
- **Implementation** (`pkg/<name>/<implname><name>/provider.go`): Contains the provider implementation, including a `NewProvider` function that returns a `factory.Provider` interface from [factory/provider.go](/pkg/factory/provider.go).
|
||||
- **Mock** (`pkg/<name>/<name>test.go`): Provides mocks for the provider, typically used by dependent packages for unit testing.
|
||||
|
||||
For example, the [prometheus](/pkg/prometheus) provider delivers a prometheus engine to the application:
|
||||
|
||||
- `pkg/prometheus/prometheus.go` - Interface definition
|
||||
- `pkg/prometheus/config.go` - Configuration
|
||||
- `pkg/prometheus/clickhouseprometheus/provider.go` - Clickhouse-powered implementation
|
||||
- `pkg/prometheus/prometheustest/provider.go` - Mock implementation
|
||||
|
||||
## How to wire it up?
|
||||
|
||||
The `pkg/signoz` package contains the inversion of control container responsible for wiring providers. It handles instantiation, configuration, and assembly of providers based on configuration metadata.
|
||||
|
||||
> 💡 **Note**: Coming from a Java background? Providers are similar to Spring beans.
|
||||
|
||||
Wiring up a provider involves three steps:
|
||||
|
||||
1. Wiring up the configuration
|
||||
Add your config from `pkg/<name>/config.go` to the `pkg/signoz/config.Config` struct and in new factories:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
...
|
||||
MyProvider myprovider.Config `mapstructure:"myprovider"`
|
||||
...
|
||||
}
|
||||
|
||||
func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, ....) (Config, error) {
|
||||
...
|
||||
configFactories := []factory.ConfigFactory{
|
||||
myprovider.NewConfigFactory(),
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
2. Wiring up the provider
|
||||
Add available provider implementations in `pkg/signoz/provider.go`:
|
||||
|
||||
```go
|
||||
func NewMyProviderFactories() factory.NamedMap[factory.ProviderFactory[myprovider.MyProvider, myprovider.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
myproviderone.NewFactory(),
|
||||
myprovidertwo.NewFactory(),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
3. Instantiate the provider by adding it to the `SigNoz` struct in `pkg/signoz/signoz.go`:
|
||||
|
||||
```go
|
||||
type SigNoz struct {
|
||||
...
|
||||
MyProvider myprovider.MyProvider
|
||||
...
|
||||
}
|
||||
|
||||
func New(...) (*SigNoz, error) {
|
||||
...
|
||||
myprovider, err := myproviderone.New(ctx, settings, config.MyProvider, "one/two")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## How to use it?
|
||||
|
||||
To use a provider, import its interface. For example, to use the prometheus provider, import `pkg/prometheus/prometheus.go`:
|
||||
|
||||
```go
|
||||
import "github.com/SigNoz/signoz/pkg/prometheus/prometheus"
|
||||
|
||||
func CreateSomething(ctx context.Context, prometheus prometheus.Prometheus) {
|
||||
...
|
||||
prometheus.DoSomething()
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Why do we need this?
|
||||
|
||||
Like any dependency injection framework, providers decouple the codebase from implementation details. This is especially valuable in SigNoz's large codebase, where we need to swap implementations without changing dependent code. The provider pattern offers several benefits apart from the obvious one of decoupling:
|
||||
|
||||
- Configuration is **defined with each provider and centralized in one place**, making it easier to understand and manage through various methods (environment variables, config files, etc.)
|
||||
- Provider mocking is **straightforward for unit testing**, with a consistent pattern for locating mocks
|
||||
- **Multiple implementations** of the same provider are **supported**, as demonstrated by our sqlstore provider
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Use the provider pattern wherever applicable.
|
||||
- Always create a provider **irrespective of the number of implementations**. This makes it easier to add new implementations in the future.
|
||||
@@ -16,7 +16,7 @@ __Table of Contents__
|
||||
- [Prerequisites](#prerequisites-1)
|
||||
- [Install Helm Repo and Charts](#install-helm-repo-and-charts)
|
||||
- [Start the OpenTelemetry Demo App](#start-the-opentelemetry-demo-app-1)
|
||||
- [Moniitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
|
||||
- [Monitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
|
||||
- [What's next](#whats-next)
|
||||
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
@@ -23,16 +25,17 @@ type provider struct {
|
||||
config licensing.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
orgGetter organization.Getter
|
||||
analytics analytics.Analytics
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
||||
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
|
||||
return New(ctx, providerSettings, config, store, zeus, orgGetter)
|
||||
return New(ctx, providerSettings, config, store, zeus, orgGetter, analytics)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) (licensing.Licensing, error) {
|
||||
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) (licensing.Licensing, error) {
|
||||
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
|
||||
licensestore := sqllicensingstore.New(sqlstore)
|
||||
return &provider{
|
||||
@@ -42,6 +45,7 @@ func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Conf
|
||||
settings: settings,
|
||||
orgGetter: orgGetter,
|
||||
stopChan: make(chan struct{}),
|
||||
analytics: analytics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -159,6 +163,25 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
|
||||
return err
|
||||
}
|
||||
|
||||
stats := licensetypes.NewStatsFromLicense(activeLicense)
|
||||
provider.analytics.Send(ctx,
|
||||
analyticstypes.Track{
|
||||
UserId: "stats_" + organizationID.String(),
|
||||
Event: "License Updated",
|
||||
Properties: analyticstypes.NewPropertiesFromMap(stats),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: organizationID.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
analyticstypes.Group{
|
||||
UserId: "stats_" + organizationID.String(),
|
||||
GroupId: organizationID.String(),
|
||||
Traits: analyticstypes.NewTraitsFromMap(stats),
|
||||
},
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -211,3 +234,16 @@ func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID va
|
||||
|
||||
return license.Features, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
activeLicense, err := provider.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return licensetypes.NewStatsFromLicense(activeLicense), nil
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ builds:
|
||||
- -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
|
||||
- -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
|
||||
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
|
||||
- -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
|
||||
- >-
|
||||
{{- if eq .Os "linux" }}-linkmode external -extldflags '-static'{{- end }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
@@ -11,11 +11,9 @@ RUN apk update && \
|
||||
|
||||
|
||||
COPY ./target/${OS}-${TARGETARCH}/signoz /root/signoz
|
||||
COPY ./conf/prometheus.yml /root/config/prometheus.yml
|
||||
COPY ./templates/email /root/templates
|
||||
COPY frontend/build/ /etc/signoz/web/
|
||||
|
||||
RUN chmod 755 /root /root/signoz
|
||||
|
||||
ENTRYPOINT ["./signoz"]
|
||||
CMD ["-config", "/root/config/prometheus.yml"]
|
||||
ENTRYPOINT ["./signoz"]
|
||||
@@ -12,11 +12,9 @@ RUN apk update && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY ./target/${OS}-${ARCH}/signoz /root/signoz
|
||||
COPY ./conf/prometheus.yml /root/config/prometheus.yml
|
||||
COPY ./templates/email /root/templates
|
||||
COPY frontend/build/ /etc/signoz/web/
|
||||
|
||||
RUN chmod 755 /root /root/signoz
|
||||
|
||||
ENTRYPOINT ["./signoz"]
|
||||
CMD ["-config", "/root/config/prometheus.yml"]
|
||||
|
||||
@@ -7,15 +7,16 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||
"github.com/SigNoz/signoz/ee/query-service/interfaces"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
querierAPI "github.com/SigNoz/signoz/pkg/querier"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
@@ -25,8 +26,7 @@ import (
|
||||
)
|
||||
|
||||
type APIHandlerOptions struct {
|
||||
DataConnector interfaces.DataConnector
|
||||
PreferSpanMetrics bool
|
||||
DataConnector interfaces.Reader
|
||||
RulesManager *rules.Manager
|
||||
UsageManager *usage.Manager
|
||||
IntegrationsController *integrations.Controller
|
||||
@@ -50,7 +50,6 @@ type APIHandler struct {
|
||||
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
|
||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||
Reader: opts.DataConnector,
|
||||
PreferSpanMetrics: opts.PreferSpanMetrics,
|
||||
RuleManager: opts.RulesManager,
|
||||
IntegrationsController: opts.IntegrationsController,
|
||||
CloudIntegrationsController: opts.CloudIntegrationsController,
|
||||
@@ -58,8 +57,9 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||
FieldsAPI: fields.NewAPI(signoz.TelemetryStore, signoz.Instrumentation.Logger()),
|
||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
||||
Signoz: signoz,
|
||||
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -96,7 +96,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email, ah.opts.JWT)
|
||||
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email)
|
||||
if err != nil {
|
||||
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
|
||||
handleSsoError(w, r, redirectUri)
|
||||
|
||||
@@ -59,7 +59,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if ah.opts.PreferSpanMetrics {
|
||||
if constants.IsPreferSpanMetrics {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.UseSpanMetrics {
|
||||
featureSet[idx].Active = true
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
basechr "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
)
|
||||
|
||||
type ClickhouseReader struct {
|
||||
conn clickhouse.Conn
|
||||
appdb sqlstore.SQLStore
|
||||
*basechr.ClickHouseReader
|
||||
}
|
||||
|
||||
func NewDataConnector(
|
||||
sqlDB sqlstore.SQLStore,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
prometheus prometheus.Prometheus,
|
||||
cluster string,
|
||||
fluxIntervalForTraceDetail time.Duration,
|
||||
cache cache.Cache,
|
||||
) *ClickhouseReader {
|
||||
chReader := basechr.NewReader(sqlDB, telemetryStore, prometheus, cluster, fluxIntervalForTraceDetail, cache)
|
||||
return &ClickhouseReader{
|
||||
conn: telemetryStore.ClickhouseDB(),
|
||||
appdb: sqlDB,
|
||||
ClickHouseReader: chReader,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ClickhouseReader) GetSQLStore() sqlstore.SQLStore {
|
||||
return r.appdb
|
||||
}
|
||||
@@ -6,14 +6,10 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof" // http profiler
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/app/api"
|
||||
"github.com/SigNoz/signoz/ee/query-service/app/db"
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||
"github.com/SigNoz/signoz/ee/query-service/rules"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
@@ -32,6 +28,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
@@ -41,7 +38,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
|
||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -59,62 +55,55 @@ type ServerOptions struct {
|
||||
Jwt *authtypes.JWT
|
||||
}
|
||||
|
||||
// Server runs HTTP api service
|
||||
// Server runs HTTP, Mux and a grpc server
|
||||
type Server struct {
|
||||
serverOptions *ServerOptions
|
||||
ruleManager *baserules.Manager
|
||||
config signoz.Config
|
||||
signoz *signoz.SigNoz
|
||||
jwt *authtypes.JWT
|
||||
ruleManager *baserules.Manager
|
||||
|
||||
// public http router
|
||||
httpConn net.Listener
|
||||
httpServer *http.Server
|
||||
httpConn net.Listener
|
||||
httpServer *http.Server
|
||||
httpHostPort string
|
||||
|
||||
// private http
|
||||
privateConn net.Listener
|
||||
privateHTTP *http.Server
|
||||
privateConn net.Listener
|
||||
privateHTTP *http.Server
|
||||
privateHostPort string
|
||||
|
||||
opampServer *opamp.Server
|
||||
|
||||
// Usage manager
|
||||
usageManager *usage.Manager
|
||||
|
||||
opampServer *opamp.Server
|
||||
|
||||
unavailableChannel chan healthcheck.Status
|
||||
}
|
||||
|
||||
// HealthCheckStatus returns health check status channel a client can subscribe to
|
||||
func (s Server) HealthCheckStatus() chan healthcheck.Status {
|
||||
return s.unavailableChannel
|
||||
}
|
||||
|
||||
// NewServer creates and initializes Server
|
||||
func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
||||
func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) (*Server, error) {
|
||||
gatewayProxy, err := gateway.NewProxy(config.Gateway.URL.String(), gateway.RoutePrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := db.NewDataConnector(
|
||||
serverOptions.SigNoz.SQLStore,
|
||||
serverOptions.SigNoz.TelemetryStore,
|
||||
serverOptions.SigNoz.Prometheus,
|
||||
serverOptions.Cluster,
|
||||
fluxIntervalForTraceDetail,
|
||||
serverOptions.SigNoz.Cache,
|
||||
reader := clickhouseReader.NewReader(
|
||||
signoz.SQLStore,
|
||||
signoz.TelemetryStore,
|
||||
signoz.Prometheus,
|
||||
signoz.TelemetryStore.Cluster(),
|
||||
config.Querier.FluxInterval,
|
||||
signoz.Cache,
|
||||
)
|
||||
|
||||
rm, err := makeRulesManager(
|
||||
serverOptions.SigNoz.SQLStore.SQLxDB(),
|
||||
reader,
|
||||
serverOptions.SigNoz.Cache,
|
||||
serverOptions.SigNoz.Alertmanager,
|
||||
serverOptions.SigNoz.SQLStore,
|
||||
serverOptions.SigNoz.TelemetryStore,
|
||||
serverOptions.SigNoz.Prometheus,
|
||||
serverOptions.SigNoz.Modules.OrgGetter,
|
||||
signoz.Cache,
|
||||
signoz.Alertmanager,
|
||||
signoz.SQLStore,
|
||||
signoz.TelemetryStore,
|
||||
signoz.Prometheus,
|
||||
signoz.Modules.OrgGetter,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -122,19 +111,16 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
// initiate opamp
|
||||
_, err = opAmpModel.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter)
|
||||
|
||||
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
|
||||
integrationsController, err := integrations.NewController(signoz.SQLStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't create integrations controller: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
|
||||
cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't create cloud provider integrations controller: %w", err,
|
||||
@@ -143,7 +129,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
|
||||
// ingestion pipelines manager
|
||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
|
||||
serverOptions.SigNoz.SQLStore, integrationsController.GetPipelinesForInstalledIntegrations,
|
||||
signoz.SQLStore,
|
||||
integrationsController.GetPipelinesForInstalledIntegrations,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -151,7 +138,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
|
||||
// initiate agent config handler
|
||||
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
|
||||
DB: serverOptions.SigNoz.SQLStore.SQLxDB(),
|
||||
Store: signoz.SQLStore,
|
||||
AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -159,7 +146,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
// start the usagemanager
|
||||
usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.OrgGetter)
|
||||
usageManager, err := usage.New(signoz.Licensing, signoz.TelemetryStore.ClickhouseDB(), signoz.Zeus, signoz.Modules.OrgGetter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -168,47 +155,36 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SetReader(reader)
|
||||
telemetry.GetInstance().SetSqlStore(serverOptions.SigNoz.SQLStore)
|
||||
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
|
||||
telemetry.GetInstance().SetSavedViewsInfoCallback(telemetry.GetSavedViewsInfo)
|
||||
telemetry.GetInstance().SetAlertsInfoCallback(telemetry.GetAlertsInfo)
|
||||
telemetry.GetInstance().SetGetUsersCallback(telemetry.GetUsers)
|
||||
telemetry.GetInstance().SetUserCountCallback(telemetry.GetUserCount)
|
||||
telemetry.GetInstance().SetDashboardsInfoCallback(telemetry.GetDashboardsInfo)
|
||||
|
||||
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiOpts := api.APIHandlerOptions{
|
||||
DataConnector: reader,
|
||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
||||
RulesManager: rm,
|
||||
UsageManager: usageManager,
|
||||
IntegrationsController: integrationsController,
|
||||
CloudIntegrationsController: cloudIntegrationsController,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
FluxInterval: fluxInterval,
|
||||
FluxInterval: config.Querier.FluxInterval,
|
||||
Gateway: gatewayProxy,
|
||||
GatewayUrl: serverOptions.GatewayUrl,
|
||||
JWT: serverOptions.Jwt,
|
||||
GatewayUrl: config.Gateway.URL.String(),
|
||||
JWT: jwt,
|
||||
}
|
||||
|
||||
apiHandler, err := api.NewAPIHandler(apiOpts, serverOptions.SigNoz)
|
||||
apiHandler, err := api.NewAPIHandler(apiOpts, signoz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
config: config,
|
||||
signoz: signoz,
|
||||
jwt: jwt,
|
||||
ruleManager: rm,
|
||||
serverOptions: serverOptions,
|
||||
httpHostPort: baseconst.HTTPHostPort,
|
||||
privateHostPort: baseconst.PrivateHostPort,
|
||||
unavailableChannel: make(chan healthcheck.Status),
|
||||
usageManager: usageManager,
|
||||
}
|
||||
|
||||
httpServer, err := s.createPublicServer(apiHandler, serverOptions.SigNoz.Web)
|
||||
httpServer, err := s.createPublicServer(apiHandler, signoz.Web)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -224,35 +200,28 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
s.privateHTTP = privateServer
|
||||
|
||||
s.opampServer = opamp.InitializeServer(
|
||||
&opAmpModel.AllAgents, agentConfMgr,
|
||||
&opAmpModel.AllAgents, agentConfMgr, signoz.Instrumentation,
|
||||
)
|
||||
|
||||
orgs, err := apiHandler.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, org := range orgs {
|
||||
errorList := reader.PreloadMetricsMetadata(context.Background(), org.ID)
|
||||
for _, er := range errorList {
|
||||
zap.L().Error("failed to preload metrics metadata", zap.Error(er))
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// HealthCheckStatus returns health check status channel a client can subscribe to
|
||||
func (s Server) HealthCheckStatus() chan healthcheck.Status {
|
||||
return s.unavailableChannel
|
||||
}
|
||||
|
||||
func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
|
||||
r := baseapp.NewRouter()
|
||||
|
||||
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
||||
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
|
||||
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
||||
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
||||
s.serverOptions.Config.APIServer.Timeout.Default,
|
||||
s.serverOptions.Config.APIServer.Timeout.Max,
|
||||
r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
|
||||
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
|
||||
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
|
||||
s.config.APIServer.Timeout.ExcludedRoutes,
|
||||
s.config.APIServer.Timeout.Default,
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewAnalytics().Wrap)
|
||||
r.Use(middleware.NewLogging(s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
|
||||
apiHandler.RegisterPrivateRoutes(r)
|
||||
|
||||
@@ -274,17 +243,16 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
||||
|
||||
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) {
|
||||
r := baseapp.NewRouter()
|
||||
am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
|
||||
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())
|
||||
|
||||
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
||||
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
|
||||
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
||||
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
||||
s.serverOptions.Config.APIServer.Timeout.Default,
|
||||
s.serverOptions.Config.APIServer.Timeout.Max,
|
||||
r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
|
||||
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
|
||||
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
|
||||
s.config.APIServer.Timeout.ExcludedRoutes,
|
||||
s.config.APIServer.Timeout.Default,
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewAnalytics().Wrap)
|
||||
r.Use(middleware.NewLogging(s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
@@ -294,6 +262,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterInfraMetricsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
apiHandler.RegisterQueryRangeV5Routes(r, am)
|
||||
apiHandler.RegisterWebSocketPaths(r, am)
|
||||
apiHandler.RegisterMessagingQueuesRoutes(r, am)
|
||||
apiHandler.RegisterThirdPartyApiRoutes(r, am)
|
||||
@@ -324,7 +293,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
func (s *Server) initListeners() error {
|
||||
// listen on public port
|
||||
var err error
|
||||
publicHostPort := s.serverOptions.HTTPHostPort
|
||||
publicHostPort := s.httpHostPort
|
||||
if publicHostPort == "" {
|
||||
return fmt.Errorf("baseconst.HTTPHostPort is required")
|
||||
}
|
||||
@@ -334,10 +303,10 @@ func (s *Server) initListeners() error {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort))
|
||||
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
|
||||
|
||||
// listen on private port to support internal services
|
||||
privateHostPort := s.serverOptions.PrivateHostPort
|
||||
privateHostPort := s.privateHostPort
|
||||
|
||||
if privateHostPort == "" {
|
||||
return fmt.Errorf("baseconst.PrivateHostPort is required")
|
||||
@@ -347,7 +316,7 @@ func (s *Server) initListeners() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort))
|
||||
zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.privateHostPort))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -367,7 +336,7 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
go func() {
|
||||
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort))
|
||||
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.httpHostPort))
|
||||
|
||||
switch err := s.httpServer.Serve(s.httpConn); err {
|
||||
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
|
||||
@@ -393,7 +362,7 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
go func() {
|
||||
zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
|
||||
zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.privateHostPort))
|
||||
|
||||
switch err := s.privateHTTP.Serve(s.privateConn); err {
|
||||
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
|
||||
@@ -445,7 +414,6 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func makeRulesManager(
|
||||
db *sqlx.DB,
|
||||
ch baseint.Reader,
|
||||
cache cache.Cache,
|
||||
alertmanager alertmanager.Alertmanager,
|
||||
@@ -458,7 +426,6 @@ func makeRulesManager(
|
||||
managerOpts := &baserules.ManagerOptions{
|
||||
TelemetryStore: telemetryStore,
|
||||
Prometheus: prometheus,
|
||||
DBConn: db,
|
||||
Context: context.Background(),
|
||||
Logger: zap.L(),
|
||||
Reader: ch,
|
||||
|
||||
@@ -37,9 +37,14 @@ func GetDefaultSiteURL() string {
|
||||
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
|
||||
|
||||
var IsDotMetricsEnabled = false
|
||||
var IsPreferSpanMetrics = false
|
||||
|
||||
func init() {
|
||||
if GetOrDefaultEnv(DotMetricsEnabled, "false") == "true" {
|
||||
IsDotMetricsEnabled = true
|
||||
}
|
||||
|
||||
if GetOrDefaultEnv("USE_SPAN_METRICS", "false") == "true" {
|
||||
IsPreferSpanMetrics = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
)
|
||||
|
||||
// Connector defines methods for interaction
|
||||
// with o11y data. for example - clickhouse
|
||||
type DataConnector interface {
|
||||
baseint.Reader
|
||||
}
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/licensing"
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/query-service/app"
|
||||
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
|
||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||
"github.com/SigNoz/signoz/ee/zeus"
|
||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/config"
|
||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||
@@ -20,6 +22,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@@ -101,10 +104,14 @@ func main() {
|
||||
fileprovider.NewFactory(),
|
||||
},
|
||||
}, signoz.DeprecatedFlags{
|
||||
MaxIdleConns: maxIdleConns,
|
||||
MaxOpenConns: maxOpenConns,
|
||||
DialTimeout: dialTimeout,
|
||||
Config: promConfigPath,
|
||||
MaxIdleConns: maxIdleConns,
|
||||
MaxOpenConns: maxOpenConns,
|
||||
DialTimeout: dialTimeout,
|
||||
Config: promConfigPath,
|
||||
FluxInterval: fluxInterval,
|
||||
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
|
||||
Cluster: cluster,
|
||||
GatewayUrl: gatewayUrl,
|
||||
})
|
||||
if err != nil {
|
||||
zap.L().Fatal("Failed to create config", zap.Error(err))
|
||||
@@ -134,12 +141,20 @@ func main() {
|
||||
zeus.Config(),
|
||||
httpzeus.NewProviderFactory(),
|
||||
licensing.Config(24*time.Hour, 3),
|
||||
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
|
||||
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter)
|
||||
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
|
||||
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(),
|
||||
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
|
||||
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {
|
||||
zap.L().Fatal("Failed to add postgressqlschema factory", zap.Error(err))
|
||||
}
|
||||
|
||||
return existingFactories
|
||||
},
|
||||
sqlStoreFactories,
|
||||
signoz.NewTelemetryStoreProviderFactories(),
|
||||
)
|
||||
@@ -147,20 +162,7 @@ func main() {
|
||||
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
||||
}
|
||||
|
||||
serverOptions := &app.ServerOptions{
|
||||
Config: config,
|
||||
SigNoz: signoz,
|
||||
HTTPHostPort: baseconst.HTTPHostPort,
|
||||
PreferSpanMetrics: preferSpanMetrics,
|
||||
PrivateHostPort: baseconst.PrivateHostPort,
|
||||
FluxInterval: fluxInterval,
|
||||
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
|
||||
Cluster: cluster,
|
||||
GatewayUrl: gatewayUrl,
|
||||
Jwt: jwt,
|
||||
}
|
||||
|
||||
server, err := app.NewServer(serverOptions)
|
||||
server, err := app.NewServer(config, signoz, jwt)
|
||||
if err != nil {
|
||||
zap.L().Fatal("Failed to create server", zap.Error(err))
|
||||
}
|
||||
|
||||
36
ee/sqlschema/postgressqlschema/formatter.go
Normal file
36
ee/sqlschema/postgressqlschema/formatter.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package postgressqlschema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
)
|
||||
|
||||
type Formatter struct {
|
||||
sqlschema.Formatter
|
||||
}
|
||||
|
||||
func (formatter Formatter) SQLDataTypeOf(dataType sqlschema.DataType) string {
|
||||
if dataType == sqlschema.DataTypeTimestamp {
|
||||
return "TIMESTAMPTZ"
|
||||
}
|
||||
|
||||
return strings.ToUpper(dataType.String())
|
||||
}
|
||||
|
||||
func (formatter Formatter) DataTypeOf(dataType string) sqlschema.DataType {
|
||||
switch strings.ToUpper(dataType) {
|
||||
case "TIMESTAMPTZ", "TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE", "TIMESTAMP WITH TIME ZONE":
|
||||
return sqlschema.DataTypeTimestamp
|
||||
case "INT8":
|
||||
return sqlschema.DataTypeBigInt
|
||||
case "INT2", "INT4", "SMALLINT", "INTEGER":
|
||||
return sqlschema.DataTypeInteger
|
||||
case "BOOL", "BOOLEAN":
|
||||
return sqlschema.DataTypeBoolean
|
||||
case "VARCHAR", "CHARACTER VARYING", "CHARACTER":
|
||||
return sqlschema.DataTypeText
|
||||
}
|
||||
|
||||
return formatter.Formatter.DataTypeOf(dataType)
|
||||
}
|
||||
285
ee/sqlschema/postgressqlschema/provider.go
Normal file
285
ee/sqlschema/postgressqlschema/provider.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package postgressqlschema
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
fmter sqlschema.SQLFormatter
|
||||
sqlstore sqlstore.SQLStore
|
||||
operator sqlschema.SQLOperator
|
||||
}
|
||||
|
||||
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("postgres"), func(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config) (sqlschema.SQLSchema, error) {
|
||||
return New(ctx, providerSettings, config, sqlstore)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config, sqlstore sqlstore.SQLStore) (sqlschema.SQLSchema, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/sqlschema/postgressqlschema")
|
||||
fmter := Formatter{Formatter: sqlschema.NewFormatter(sqlstore.BunDB().Dialect())}
|
||||
|
||||
return &provider{
|
||||
sqlstore: sqlstore,
|
||||
fmter: fmter,
|
||||
settings: settings,
|
||||
operator: sqlschema.NewOperator(fmter, sqlschema.OperatorSupport{
|
||||
DropConstraint: true,
|
||||
ColumnIfNotExistsExists: true,
|
||||
AlterColumnSetNotNull: true,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Formatter() sqlschema.SQLFormatter {
|
||||
return provider.fmter
|
||||
}
|
||||
|
||||
func (provider *provider) Operator() sqlschema.SQLOperator {
|
||||
return provider.operator
|
||||
}
|
||||
|
||||
func (provider *provider) GetTable(ctx context.Context, tableName sqlschema.TableName) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
|
||||
rows, err := provider.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
QueryContext(ctx, `
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.is_nullable = 'YES',
|
||||
c.udt_name,
|
||||
c.column_default
|
||||
FROM
|
||||
information_schema.columns AS c
|
||||
WHERE
|
||||
c.table_name = ?`, string(tableName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
columns := make([]*sqlschema.Column, 0)
|
||||
for rows.Next() {
|
||||
var (
|
||||
name string
|
||||
sqlDataType string
|
||||
nullable bool
|
||||
defaultVal *string
|
||||
)
|
||||
if err := rows.Scan(&name, &nullable, &sqlDataType, &defaultVal); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
columnDefault := ""
|
||||
if defaultVal != nil {
|
||||
columnDefault = *defaultVal
|
||||
}
|
||||
|
||||
columns = append(columns, &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName(name),
|
||||
Nullable: nullable,
|
||||
DataType: provider.fmter.DataTypeOf(sqlDataType),
|
||||
Default: columnDefault,
|
||||
})
|
||||
}
|
||||
|
||||
constraintsRows, err := provider.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
QueryContext(ctx, `
|
||||
SELECT
|
||||
c.column_name,
|
||||
constraint_name,
|
||||
constraint_type
|
||||
FROM
|
||||
information_schema.table_constraints tc
|
||||
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_catalog, table_name, constraint_name)
|
||||
JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
|
||||
WHERE
|
||||
c.table_name = ?`, string(tableName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := constraintsRows.Close(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var primaryKeyConstraint *sqlschema.PrimaryKeyConstraint
|
||||
uniqueConstraintsMap := make(map[string]*sqlschema.UniqueConstraint)
|
||||
for constraintsRows.Next() {
|
||||
var (
|
||||
name string
|
||||
constraintName string
|
||||
constraintType string
|
||||
)
|
||||
|
||||
if err := constraintsRows.Scan(&name, &constraintName, &constraintType); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if constraintType == "PRIMARY KEY" {
|
||||
if primaryKeyConstraint == nil {
|
||||
primaryKeyConstraint = (&sqlschema.PrimaryKeyConstraint{
|
||||
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
|
||||
}).Named(constraintName).(*sqlschema.PrimaryKeyConstraint)
|
||||
} else {
|
||||
primaryKeyConstraint.ColumnNames = append(primaryKeyConstraint.ColumnNames, sqlschema.ColumnName(name))
|
||||
}
|
||||
}
|
||||
|
||||
if constraintType == "UNIQUE" {
|
||||
if _, ok := uniqueConstraintsMap[constraintName]; !ok {
|
||||
uniqueConstraintsMap[constraintName] = (&sqlschema.UniqueConstraint{
|
||||
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
|
||||
}).Named(constraintName).(*sqlschema.UniqueConstraint)
|
||||
} else {
|
||||
uniqueConstraintsMap[constraintName].ColumnNames = append(uniqueConstraintsMap[constraintName].ColumnNames, sqlschema.ColumnName(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreignKeyConstraintsRows, err := provider.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
QueryContext(ctx, `
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
kcu.table_name AS referencing_table,
|
||||
kcu.column_name AS referencing_column,
|
||||
ccu.table_name AS referenced_table,
|
||||
ccu.column_name AS referenced_column
|
||||
FROM
|
||||
information_schema.key_column_usage kcu
|
||||
JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
||||
WHERE
|
||||
tc.constraint_type = ?
|
||||
AND kcu.table_name = ?`, "FOREIGN KEY", string(tableName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := foreignKeyConstraintsRows.Close(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
foreignKeyConstraints := make([]*sqlschema.ForeignKeyConstraint, 0)
|
||||
for foreignKeyConstraintsRows.Next() {
|
||||
var (
|
||||
constraintName string
|
||||
referencingTable string
|
||||
referencingColumn string
|
||||
referencedTable string
|
||||
referencedColumn string
|
||||
)
|
||||
|
||||
if err := foreignKeyConstraintsRows.Scan(&constraintName, &referencingTable, &referencingColumn, &referencedTable, &referencedColumn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
foreignKeyConstraints = append(foreignKeyConstraints, (&sqlschema.ForeignKeyConstraint{
|
||||
ReferencingColumnName: sqlschema.ColumnName(referencingColumn),
|
||||
ReferencedTableName: sqlschema.TableName(referencedTable),
|
||||
ReferencedColumnName: sqlschema.ColumnName(referencedColumn),
|
||||
}).Named(constraintName).(*sqlschema.ForeignKeyConstraint))
|
||||
}
|
||||
|
||||
uniqueConstraints := make([]*sqlschema.UniqueConstraint, 0)
|
||||
for _, uniqueConstraint := range uniqueConstraintsMap {
|
||||
uniqueConstraints = append(uniqueConstraints, uniqueConstraint)
|
||||
}
|
||||
|
||||
return &sqlschema.Table{
|
||||
Name: tableName,
|
||||
Columns: columns,
|
||||
PrimaryKeyConstraint: primaryKeyConstraint,
|
||||
ForeignKeyConstraints: foreignKeyConstraints,
|
||||
}, uniqueConstraints, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetIndices(ctx context.Context, name sqlschema.TableName) ([]sqlschema.Index, error) {
|
||||
rows, err := provider.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
QueryContext(ctx, `
|
||||
SELECT
|
||||
ct.relname AS table_name,
|
||||
ci.relname AS index_name,
|
||||
i.indisunique AS unique,
|
||||
i.indisprimary AS primary,
|
||||
a.attname AS column_name
|
||||
FROM
|
||||
pg_index i
|
||||
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
|
||||
LEFT JOIN pg_class ci ON ci.oid = i.indexrelid
|
||||
LEFT JOIN pg_attribute a ON a.attrelid = ct.oid
|
||||
LEFT JOIN pg_constraint con ON con.conindid = i.indexrelid
|
||||
WHERE
|
||||
a.attnum = ANY(i.indkey)
|
||||
AND con.oid IS NULL
|
||||
AND ct.relkind = 'r'
|
||||
AND ct.relname = ?`, string(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
uniqueIndicesMap := make(map[string]*sqlschema.UniqueIndex)
|
||||
for rows.Next() {
|
||||
var (
|
||||
tableName string
|
||||
indexName string
|
||||
unique bool
|
||||
primary bool
|
||||
columnName string
|
||||
)
|
||||
|
||||
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if unique {
|
||||
if _, ok := uniqueIndicesMap[indexName]; !ok {
|
||||
uniqueIndicesMap[indexName] = &sqlschema.UniqueIndex{
|
||||
TableName: name,
|
||||
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
|
||||
}
|
||||
} else {
|
||||
uniqueIndicesMap[indexName].ColumnNames = append(uniqueIndicesMap[indexName].ColumnNames, sqlschema.ColumnName(columnName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indices := make([]sqlschema.Index, 0)
|
||||
for _, index := range uniqueIndicesMap {
|
||||
indices = append(indices, index)
|
||||
}
|
||||
|
||||
return indices, nil
|
||||
}
|
||||
|
||||
func (provider *provider) ToggleFKEnforcement(_ context.Context, _ bun.IDB, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
@@ -17,19 +17,21 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
Org = "org"
|
||||
User = "user"
|
||||
UserNoCascade = "user_no_cascade"
|
||||
FactorPassword = "factor_password"
|
||||
CloudIntegration = "cloud_integration"
|
||||
Org = "org"
|
||||
User = "user"
|
||||
UserNoCascade = "user_no_cascade"
|
||||
FactorPassword = "factor_password"
|
||||
CloudIntegration = "cloud_integration"
|
||||
AgentConfigVersion = "agent_config_version"
|
||||
)
|
||||
|
||||
var (
|
||||
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
|
||||
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
||||
UserReferenceNoCascade = `("user_id") REFERENCES "users" ("id")`
|
||||
FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
|
||||
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
||||
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
|
||||
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
||||
UserReferenceNoCascade = `("user_id") REFERENCES "users" ("id")`
|
||||
FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
|
||||
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
||||
AgentConfigVersionReference = `("version_id") REFERENCES "agent_config_version" ("id")`
|
||||
)
|
||||
|
||||
type dialect struct{}
|
||||
@@ -274,6 +276,8 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
|
||||
fkReferences = append(fkReferences, FactorPasswordReference)
|
||||
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
||||
fkReferences = append(fkReferences, CloudIntegrationReference)
|
||||
} else if reference == AgentConfigVersion && !slices.Contains(fkReferences, AgentConfigVersionReference) {
|
||||
fkReferences = append(fkReferences, AgentConfigVersionReference)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/pgdialect"
|
||||
)
|
||||
@@ -19,7 +18,6 @@ type provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
sqldb *sql.DB
|
||||
bundb *sqlstore.BunDB
|
||||
sqlxdb *sqlx.DB
|
||||
dialect *dialect
|
||||
}
|
||||
|
||||
@@ -61,7 +59,6 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
||||
settings: settings,
|
||||
sqldb: sqldb,
|
||||
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
|
||||
sqlxdb: sqlx.NewDb(sqldb, "postgres"),
|
||||
dialect: new(dialect),
|
||||
}, nil
|
||||
}
|
||||
@@ -74,10 +71,6 @@ func (provider *provider) SQLDB() *sql.DB {
|
||||
return provider.sqldb
|
||||
}
|
||||
|
||||
func (provider *provider) SQLxDB() *sqlx.DB {
|
||||
return provider.sqlxdb
|
||||
}
|
||||
|
||||
func (provider *provider) Dialect() sqlstore.SQLDialect {
|
||||
return provider.dialect
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fontfaceobserver": "2.3.0",
|
||||
"history": "4.10.1",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"http-proxy-middleware": "3.0.3",
|
||||
"http-proxy-middleware": "3.0.5",
|
||||
"http-status-codes": "2.3.0",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
@@ -134,7 +134,7 @@
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-dev-server": "^4.15.2",
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||
"xstate": "^4.31.0"
|
||||
},
|
||||
@@ -197,7 +197,6 @@
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"compression-webpack-plugin": "9.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"critters-webpack-plugin": "^3.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.4",
|
||||
@@ -235,7 +234,7 @@
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.(js|jsx|ts|tsx)": [
|
||||
@@ -251,7 +250,7 @@
|
||||
"xml2js": "0.5.0",
|
||||
"phin": "^3.7.1",
|
||||
"body-parser": "1.20.3",
|
||||
"http-proxy-middleware": "3.0.3",
|
||||
"http-proxy-middleware": "3.0.5",
|
||||
"cross-spawn": "7.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"serialize-javascript": "6.0.2",
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
||||
"alert_form_step3": "Step {{step}} - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"tooltip_notification_channels": "More details on how to setting notification channels",
|
||||
"sending_channels_note": "The alerts will be sent to all the configured channels.",
|
||||
"loading_channels_message": "Loading Channels..",
|
||||
"page_title_create": "New Notification Channels",
|
||||
"page_title_edit": "Edit Notification Channels",
|
||||
"page_title_create": "New Notification Channel",
|
||||
"page_title_edit": "Edit Notification Channel",
|
||||
"button_save_channel": "Save",
|
||||
"button_test_channel": "Test",
|
||||
"button_return": "Back",
|
||||
@@ -62,5 +62,8 @@
|
||||
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
||||
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
||||
"webhook_url_required": "Webhook URL is mandatory",
|
||||
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
|
||||
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
|
||||
"api_key_required": "API Key is mandatory",
|
||||
"to_required": "To field is mandatory",
|
||||
"channel_name_required": "Channel name is mandatory"
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
||||
"alert_form_step3": "Step {{step}} - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
|
||||
@@ -129,5 +129,6 @@
|
||||
"text_num_points": "data points in each result group",
|
||||
"text_alert_frequency": "Run alert every",
|
||||
"text_for": "minutes",
|
||||
"selected_query_placeholder": "Select query"
|
||||
"selected_query_placeholder": "Select query",
|
||||
"alert_rule_not_found": "Alert Rule not found"
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"tooltip_notification_channels": "More details on how to setting notification channels",
|
||||
"sending_channels_note": "The alerts will be sent to all the configured channels.",
|
||||
"loading_channels_message": "Loading Channels..",
|
||||
"page_title_create": "New Notification Channels",
|
||||
"page_title_edit": "Edit Notification Channels",
|
||||
"page_title_create": "New Notification Channel",
|
||||
"page_title_edit": "Edit Notification Channel",
|
||||
"button_save_channel": "Save",
|
||||
"button_test_channel": "Test",
|
||||
"button_return": "Back",
|
||||
@@ -77,5 +77,8 @@
|
||||
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
||||
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
||||
"webhook_url_required": "Webhook URL is mandatory",
|
||||
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
|
||||
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
|
||||
"api_key_required": "API Key is mandatory",
|
||||
"to_required": "To field is mandatory",
|
||||
"channel_name_required": "Channel name is mandatory"
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
||||
"alert_form_step3": "Step {{step}} - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
|
||||
@@ -3,6 +3,7 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import getAll from 'api/v1/user/get';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
@@ -14,6 +15,7 @@ import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
|
||||
import { OrgPreference } from 'types/api/preferences/preference';
|
||||
import { Organization } from 'types/api/user/getOrganization';
|
||||
import { UserResponse } from 'types/api/user/getUser';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
@@ -95,7 +97,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
usersData.data
|
||||
) {
|
||||
const isOnboardingComplete = orgPreferences?.find(
|
||||
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
|
||||
(preference: OrgPreference) =>
|
||||
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
|
||||
)?.value;
|
||||
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
@@ -123,7 +126,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
|
||||
const isRouteEnabledForWorkspaceBlockedState =
|
||||
isAdmin &&
|
||||
(path === ROUTES.ORG_SETTINGS ||
|
||||
(path === ROUTES.SETTINGS ||
|
||||
path === ROUTES.ORG_SETTINGS ||
|
||||
path === ROUTES.BILLING ||
|
||||
path === ROUTES.MY_SETTINGS);
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ function App(): JSX.Element {
|
||||
const orgName =
|
||||
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
|
||||
|
||||
const { displayName, email, role } = user;
|
||||
const { displayName, email, role, id, orgId } = user;
|
||||
|
||||
const domain = extractDomain(email);
|
||||
const hostNameParts = hostname.split('.');
|
||||
@@ -105,7 +105,7 @@ function App(): JSX.Element {
|
||||
logEvent('Domain Identified', groupTraits, 'group');
|
||||
}
|
||||
if (window && window.Appcues) {
|
||||
window.Appcues.identify(email, {
|
||||
window.Appcues.identify(id, {
|
||||
name: displayName,
|
||||
|
||||
tenant_id: hostNameParts[0],
|
||||
@@ -131,7 +131,7 @@ function App(): JSX.Element {
|
||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||
});
|
||||
|
||||
posthog?.identify(email, {
|
||||
posthog?.identify(id, {
|
||||
email,
|
||||
name: displayName,
|
||||
orgName,
|
||||
@@ -143,7 +143,7 @@ function App(): JSX.Element {
|
||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||
});
|
||||
|
||||
posthog?.group('company', domain, {
|
||||
posthog?.group('company', orgId, {
|
||||
name: orgName,
|
||||
tenant_id: hostNameParts[0],
|
||||
data_region: hostNameParts[1],
|
||||
@@ -191,19 +191,22 @@ function App(): JSX.Element {
|
||||
// if the user is on basic plan then remove billing
|
||||
if (isOnBasicPlan) {
|
||||
updatedRoutes = updatedRoutes.filter(
|
||||
(route) => route?.path !== ROUTES.BILLING,
|
||||
(route) =>
|
||||
route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
|
||||
);
|
||||
|
||||
if (isEnterpriseSelfHostedUser) {
|
||||
updatedRoutes.push(LIST_LICENSES);
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnterpriseSelfHostedUser) {
|
||||
updatedRoutes.push(LIST_LICENSES);
|
||||
}
|
||||
|
||||
// always add support route for cloud users
|
||||
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
|
||||
} else {
|
||||
// if not a cloud user then remove billing and add list licenses route
|
||||
updatedRoutes = updatedRoutes.filter(
|
||||
(route) => route?.path !== ROUTES.BILLING,
|
||||
(route) =>
|
||||
route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
|
||||
);
|
||||
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
|
||||
}
|
||||
|
||||
@@ -128,12 +128,7 @@ export const AlertOverview = Loadable(
|
||||
);
|
||||
|
||||
export const CreateAlertChannelAlerts = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
|
||||
);
|
||||
|
||||
export const EditAlertChannelsAlerts = Loadable(
|
||||
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/ChannelsEdit'),
|
||||
() => import(/* webpackChunkName: "Create Channels" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const AllAlertChannels = Loadable(
|
||||
@@ -165,7 +160,7 @@ export const APIKeys = Loadable(
|
||||
);
|
||||
|
||||
export const MySettings = Loadable(
|
||||
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
|
||||
() => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const CustomDomainSettings = Loadable(
|
||||
@@ -222,7 +217,7 @@ export const LogsIndexToFields = Loadable(
|
||||
);
|
||||
|
||||
export const BillingPage = Loadable(
|
||||
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'),
|
||||
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const SupportPage = Loadable(
|
||||
@@ -249,7 +244,7 @@ export const WorkspaceAccessRestricted = Loadable(
|
||||
);
|
||||
|
||||
export const ShortcutsPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
|
||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const InstalledIntegrations = Loadable(
|
||||
|
||||
@@ -7,20 +7,15 @@ import {
|
||||
AlertOverview,
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
APIKeys,
|
||||
ApiMonitoring,
|
||||
BillingPage,
|
||||
CreateAlertChannelAlerts,
|
||||
CreateNewAlerts,
|
||||
CustomDomainSettings,
|
||||
DashboardPage,
|
||||
DashboardWidget,
|
||||
EditAlertChannelsAlerts,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
Home,
|
||||
InfrastructureMonitoring,
|
||||
IngestionSettings,
|
||||
InstalledIntegrations,
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
@@ -31,12 +26,10 @@ import {
|
||||
LogsIndexToFields,
|
||||
LogsSaveViews,
|
||||
MetricsExplorer,
|
||||
MySettings,
|
||||
NewDashboardPage,
|
||||
OldLogsExplorer,
|
||||
Onboarding,
|
||||
OnboardingV2,
|
||||
OrganizationSettings,
|
||||
OrgOnboarding,
|
||||
PasswordReset,
|
||||
PipelinePage,
|
||||
@@ -45,7 +38,6 @@ import {
|
||||
ServicesTablePage,
|
||||
ServiceTopLevelOperationsPage,
|
||||
SettingsPage,
|
||||
ShortcutsPage,
|
||||
SignupPage,
|
||||
SomethingWentWrong,
|
||||
StatusPage,
|
||||
@@ -150,7 +142,7 @@ const routes: AppRoutes[] = [
|
||||
},
|
||||
{
|
||||
path: ROUTES.SETTINGS,
|
||||
exact: true,
|
||||
exact: false,
|
||||
component: SettingsPage,
|
||||
isPrivate: true,
|
||||
key: 'SETTINGS',
|
||||
@@ -260,13 +252,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'CHANNELS_NEW',
|
||||
},
|
||||
{
|
||||
path: ROUTES.CHANNELS_EDIT,
|
||||
exact: true,
|
||||
component: EditAlertChannelsAlerts,
|
||||
isPrivate: true,
|
||||
key: 'CHANNELS_EDIT',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALL_CHANNELS,
|
||||
exact: true,
|
||||
@@ -295,41 +280,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'VERSION',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ORG_SETTINGS,
|
||||
exact: true,
|
||||
component: OrganizationSettings,
|
||||
isPrivate: true,
|
||||
key: 'ORG_SETTINGS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INGESTION_SETTINGS,
|
||||
exact: true,
|
||||
component: IngestionSettings,
|
||||
isPrivate: true,
|
||||
key: 'INGESTION_SETTINGS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.API_KEYS,
|
||||
exact: true,
|
||||
component: APIKeys,
|
||||
isPrivate: true,
|
||||
key: 'API_KEYS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.MY_SETTINGS,
|
||||
exact: true,
|
||||
component: MySettings,
|
||||
isPrivate: true,
|
||||
key: 'MY_SETTINGS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.CUSTOM_DOMAIN_SETTINGS,
|
||||
exact: true,
|
||||
component: CustomDomainSettings,
|
||||
isPrivate: true,
|
||||
key: 'CUSTOM_DOMAIN_SETTINGS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGS,
|
||||
exact: true,
|
||||
@@ -393,13 +343,6 @@ const routes: AppRoutes[] = [
|
||||
key: 'SOMETHING_WENT_WRONG',
|
||||
isPrivate: false,
|
||||
},
|
||||
{
|
||||
path: ROUTES.BILLING,
|
||||
exact: true,
|
||||
component: BillingPage,
|
||||
key: 'BILLING',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.WORKSPACE_LOCKED,
|
||||
exact: true,
|
||||
@@ -421,13 +364,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'WORKSPACE_ACCESS_RESTRICTED',
|
||||
},
|
||||
{
|
||||
path: ROUTES.SHORTCUTS,
|
||||
exact: true,
|
||||
component: ShortcutsPage,
|
||||
isPrivate: true,
|
||||
key: 'SHORTCUTS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INTEGRATIONS,
|
||||
exact: true,
|
||||
|
||||
29
frontend/src/api/changelog/getChangelogByVersion.ts
Normal file
29
frontend/src/api/changelog/getChangelogByVersion.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||
|
||||
const getChangelogByVersion = async (
|
||||
versionId: string,
|
||||
): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`
|
||||
https://cms.signoz.cloud/api/release-changelogs?filters[version][$eq]=${versionId}&populate[features][sort]=sort_order:asc&populate[features][populate][media][fields]=id,ext,url,mime,alternativeText
|
||||
`);
|
||||
|
||||
if (!Array.isArray(response.data.data) || response.data.data.length === 0) {
|
||||
throw new Error('No changelog found!');
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.statusText,
|
||||
payload: response.data.data[0],
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getChangelogByVersion;
|
||||
@@ -1,18 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const getAllOrgPreferences = async (): Promise<
|
||||
SuccessResponse<GetAllOrgPreferencesResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.get(`/org/preferences`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getAllOrgPreferences;
|
||||
@@ -1,18 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const getAllUserPreferences = async (): Promise<
|
||||
SuccessResponse<GetAllUserPreferencesResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.get(`/user/preferences`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getAllUserPreferences;
|
||||
@@ -1,20 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const getOrgPreference = async ({
|
||||
preferenceID,
|
||||
}: {
|
||||
preferenceID: string;
|
||||
}): Promise<SuccessResponse<GetOrgPreferenceResponseProps> | ErrorResponse> => {
|
||||
const response = await axios.get(`/org/preferences/${preferenceID}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getOrgPreference;
|
||||
@@ -1,22 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const getUserPreference = async ({
|
||||
preferenceID,
|
||||
}: {
|
||||
preferenceID: string;
|
||||
}): Promise<
|
||||
SuccessResponse<GetUserPreferenceResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.get(`/user/preferences/${preferenceID}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getUserPreference;
|
||||
@@ -1,28 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
UpdateOrgPreferenceProps,
|
||||
UpdateOrgPreferenceResponseProps,
|
||||
} from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const updateOrgPreference = async (
|
||||
preferencePayload: UpdateOrgPreferenceProps,
|
||||
): Promise<
|
||||
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.put(
|
||||
`/org/preferences/${preferencePayload.preferenceID}`,
|
||||
{
|
||||
preference_value: preferencePayload.value,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateOrgPreference;
|
||||
@@ -1,28 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
UpdateUserPreferenceProps,
|
||||
UpdateUserPreferenceResponseProps,
|
||||
} from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const updateUserPreference = async (
|
||||
preferencePayload: UpdateUserPreferenceProps,
|
||||
): Promise<
|
||||
SuccessResponse<UpdateUserPreferenceResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.put(
|
||||
`/user/preferences/${preferencePayload.preferenceID}`,
|
||||
{
|
||||
preference_value: preferencePayload.value,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateUserPreference;
|
||||
@@ -119,6 +119,7 @@ export const updateFunnelSteps = async (
|
||||
export interface ValidateFunnelPayload {
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface ValidateFunnelResponse {
|
||||
@@ -132,12 +133,11 @@ export interface ValidateFunnelResponse {
|
||||
}
|
||||
|
||||
export const validateFunnelSteps = async (
|
||||
funnelId: string,
|
||||
payload: ValidateFunnelPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<ValidateFunnelResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/validate`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/validate`,
|
||||
payload,
|
||||
{ signal },
|
||||
);
|
||||
@@ -185,6 +185,7 @@ export interface FunnelOverviewPayload {
|
||||
end_time: number;
|
||||
step_start?: number;
|
||||
step_end?: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface FunnelOverviewResponse {
|
||||
@@ -196,20 +197,17 @@ export interface FunnelOverviewResponse {
|
||||
avg_rate: number;
|
||||
conversion_rate: number | null;
|
||||
errors: number;
|
||||
// TODO(shaheer): remove p99_latency once we have support for latency
|
||||
p99_latency: number;
|
||||
latency: number;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export const getFunnelOverview = async (
|
||||
funnelId: string,
|
||||
payload: FunnelOverviewPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<FunnelOverviewResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/overview`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/overview`,
|
||||
payload,
|
||||
{
|
||||
signal,
|
||||
@@ -237,12 +235,11 @@ export interface SlowTraceData {
|
||||
}
|
||||
|
||||
export const getFunnelSlowTraces = async (
|
||||
funnelId: string,
|
||||
payload: FunnelOverviewPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/slow-traces`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/slow-traces`,
|
||||
payload,
|
||||
{
|
||||
signal,
|
||||
@@ -275,7 +272,7 @@ export const getFunnelErrorTraces = async (
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
|
||||
const response: AxiosResponse = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/error-traces`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/error-traces`,
|
||||
payload,
|
||||
{
|
||||
signal,
|
||||
@@ -293,6 +290,7 @@ export const getFunnelErrorTraces = async (
|
||||
export interface FunnelStepsPayload {
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface FunnelStepGraphMetrics {
|
||||
@@ -309,12 +307,11 @@ export interface FunnelStepsResponse {
|
||||
}
|
||||
|
||||
export const getFunnelSteps = async (
|
||||
funnelId: string,
|
||||
payload: FunnelStepsPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<FunnelStepsResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/steps`,
|
||||
payload,
|
||||
{ signal },
|
||||
);
|
||||
@@ -332,6 +329,7 @@ export interface FunnelStepsOverviewPayload {
|
||||
end_time: number;
|
||||
step_start?: number;
|
||||
step_end?: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface FunnelStepsOverviewResponse {
|
||||
@@ -343,12 +341,11 @@ export interface FunnelStepsOverviewResponse {
|
||||
}
|
||||
|
||||
export const getFunnelStepsOverview = async (
|
||||
funnelId: string,
|
||||
payload: FunnelStepsOverviewPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps/overview`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/steps/overview`,
|
||||
payload,
|
||||
{ signal },
|
||||
);
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { Props } from 'types/api/userFeedback/sendResponse';
|
||||
|
||||
const sendFeedback = async (props: Props): Promise<number> => {
|
||||
const response = await axios.post(
|
||||
'/feedback',
|
||||
{
|
||||
email: props.email,
|
||||
message: props.message,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.status;
|
||||
};
|
||||
|
||||
export default sendFeedback;
|
||||
23
frontend/src/api/v1/org/preferences/list.ts
Normal file
23
frontend/src/api/v1/org/preferences/list.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/preferences/list';
|
||||
import { OrgPreference } from 'types/api/preferences/preference';
|
||||
|
||||
const listPreference = async (): Promise<
|
||||
SuccessResponseV2<OrgPreference[]>
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/org/preferences`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default listPreference;
|
||||
25
frontend/src/api/v1/org/preferences/name/get.ts
Normal file
25
frontend/src/api/v1/org/preferences/name/get.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/preferences/get';
|
||||
import { OrgPreference } from 'types/api/preferences/preference';
|
||||
|
||||
const getPreference = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<OrgPreference>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(
|
||||
`/org/preferences/${props.name}`,
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getPreference;
|
||||
22
frontend/src/api/v1/org/preferences/name/update.ts
Normal file
22
frontend/src/api/v1/org/preferences/name/update.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Props } from 'types/api/preferences/update';
|
||||
|
||||
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.put(`/org/preferences/${props.name}`, {
|
||||
value: props.value,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
@@ -1,24 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/user/getUserPreference';
|
||||
|
||||
const getPreference = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get(`/user/preferences`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getPreference;
|
||||
21
frontend/src/api/v1/user/preferences/list.ts
Normal file
21
frontend/src/api/v1/user/preferences/list.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/preferences/list';
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
|
||||
const list = async (): Promise<SuccessResponseV2<UserPreference[]>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/user/preferences`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default list;
|
||||
25
frontend/src/api/v1/user/preferences/name/get.ts
Normal file
25
frontend/src/api/v1/user/preferences/name/get.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/preferences/get';
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
|
||||
const get = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<UserPreference>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(
|
||||
`/user/preferences/${props.name}`,
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
22
frontend/src/api/v1/user/preferences/name/update.ts
Normal file
22
frontend/src/api/v1/user/preferences/name/update.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Props } from 'types/api/preferences/update';
|
||||
|
||||
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.put(`/user/preferences/${props.name}`, {
|
||||
value: props.value,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
@@ -0,0 +1,161 @@
|
||||
.changelog-modal {
|
||||
.ant-modal-content {
|
||||
padding: unset;
|
||||
background-color: var(--bg-ink-400, #121317);
|
||||
|
||||
.ant-modal-header {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
margin-top: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background-color: var(--bg-ink-400, #121317);
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-vanilla-100, #fff);
|
||||
border-bottom: 1px solid var(--bg-slate-500, #161922);
|
||||
}
|
||||
|
||||
&-footer.scroll-available {
|
||||
.scroll-btn-container {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
position: relative;
|
||||
border: 1px solid var(--bg-slate-500, #161922);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&-label {
|
||||
color: var(--text-robin-400, #7190f9);
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
position: relative;
|
||||
padding-left: 14px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
background-color: var(--bg-robin-500, #7190f9);
|
||||
}
|
||||
}
|
||||
|
||||
&-ctas {
|
||||
display: flex;
|
||||
|
||||
& svg {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-btn-container {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.scroll-btn {
|
||||
all: unset;
|
||||
padding: 4px 12px 4px 10px;
|
||||
background-color: var(--bg-slate-400, #1d212d);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
transition: background-color 0.1s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-200, #2c3140);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--bg-slate-600, #1c1f2a);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--text-vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
// add animation to the chevrons down icon
|
||||
svg {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--bg-slate-500, #161922);
|
||||
border-top-width: 0;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// pulse for the scroll for more icon
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.changelog-modal {
|
||||
.ant-modal-content {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&-title {
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-500);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&-content {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&-footer {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
.scroll-btn-container {
|
||||
.scroll-btn {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
|
||||
span {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
131
frontend/src/components/ChangelogModal/ChangelogModal.tsx
Normal file
131
frontend/src/components/ChangelogModal/ChangelogModal.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import './ChangelogModal.styles.scss';
|
||||
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import { ChevronsDown, ScrollText } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import ChangelogRenderer from './components/ChangelogRenderer';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ChangelogModal({ onClose }: Props): JSX.Element {
|
||||
const [hasScroll, setHasScroll] = useState(false);
|
||||
const changelogContentSectionRef = useRef<HTMLDivElement>(null);
|
||||
const { changelog } = useAppContext();
|
||||
|
||||
const formattedReleaseDate = dayjs(changelog?.release_date).format(
|
||||
'MMMM D, YYYY',
|
||||
);
|
||||
|
||||
const checkScroll = useCallback((): void => {
|
||||
if (changelogContentSectionRef.current) {
|
||||
const {
|
||||
scrollHeight,
|
||||
clientHeight,
|
||||
scrollTop,
|
||||
} = changelogContentSectionRef.current;
|
||||
const isAtBottom = scrollHeight - clientHeight - scrollTop <= 8;
|
||||
setHasScroll(scrollHeight > clientHeight + 24 && !isAtBottom); // 24px - buffer height to show show more
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkScroll();
|
||||
const changelogContentSection = changelogContentSectionRef.current;
|
||||
|
||||
if (changelogContentSection) {
|
||||
changelogContentSection.addEventListener('scroll', checkScroll);
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
if (changelogContentSection) {
|
||||
changelogContentSection.removeEventListener('scroll', checkScroll);
|
||||
}
|
||||
};
|
||||
}, [checkScroll]);
|
||||
|
||||
const onClickUpdateWorkspace = (): void => {
|
||||
window.open(
|
||||
'https://github.com/SigNoz/signoz/releases',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
};
|
||||
|
||||
const onClickScrollForMore = (): void => {
|
||||
if (changelogContentSectionRef.current) {
|
||||
changelogContentSectionRef.current.scrollTo({
|
||||
top: changelogContentSectionRef.current.scrollTop + 600, // Scroll 600px from the current position
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={cx('changelog-modal')}
|
||||
title={
|
||||
<div className="changelog-modal-title">
|
||||
<ScrollText size={16} />
|
||||
What’s New ⎯ Changelog : {formattedReleaseDate}
|
||||
</div>
|
||||
}
|
||||
width={820}
|
||||
open
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<div
|
||||
className={cx('changelog-modal-footer', hasScroll && 'scroll-available')}
|
||||
>
|
||||
{changelog?.features && changelog.features.length > 0 && (
|
||||
<span className="changelog-modal-footer-label">
|
||||
{changelog.features.length} new
|
||||
{changelog.features.length > 1 ? 'features' : 'feature'}
|
||||
</span>
|
||||
)}
|
||||
<div className="changelog-modal-footer-ctas">
|
||||
<Button type="default" icon={<CloseOutlined />} onClick={onClose}>
|
||||
Skip for now
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
onClick={onClickUpdateWorkspace}
|
||||
>
|
||||
Update my workspace
|
||||
</Button>
|
||||
</div>
|
||||
{changelog && (
|
||||
<div className="scroll-btn-container">
|
||||
<button
|
||||
data-testid="scroll-more-btn"
|
||||
type="button"
|
||||
className="scroll-btn"
|
||||
onClick={onClickScrollForMore}
|
||||
>
|
||||
<ChevronsDown size={14} />
|
||||
<span>Scroll for more</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="changelog-modal-content"
|
||||
data-testid="changelog-content"
|
||||
ref={changelogContentSectionRef}
|
||||
>
|
||||
{changelog && <ChangelogRenderer changelog={changelog} />}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangelogModal;
|
||||
@@ -0,0 +1,79 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import ChangelogModal from '../ChangelogModal';
|
||||
|
||||
const mockChangelog = {
|
||||
release_date: '2025-06-10',
|
||||
features: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Feature 1',
|
||||
description: 'Description for feature 1',
|
||||
media: null,
|
||||
},
|
||||
],
|
||||
bug_fixes: 'Bug fix details',
|
||||
maintenance: 'Maintenance details',
|
||||
};
|
||||
|
||||
// Mock react-markdown to just render children as plain text
|
||||
jest.mock(
|
||||
'react-markdown',
|
||||
() =>
|
||||
function ReactMarkdown({ children }: any) {
|
||||
return <div>{children}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
// mock useAppContext
|
||||
jest.mock('providers/App/App', () => ({
|
||||
useAppContext: jest.fn(() => ({ changelog: mockChangelog })),
|
||||
}));
|
||||
|
||||
describe('ChangelogModal', () => {
|
||||
it('renders modal with changelog data', () => {
|
||||
render(<ChangelogModal onClose={jest.fn()} />);
|
||||
expect(
|
||||
screen.getByText('What’s New ⎯ Changelog : June 10, 2025'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Feature 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Description for feature 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Bug fix details')).toBeInTheDocument();
|
||||
expect(screen.getByText('Maintenance details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when Skip for now is clicked', () => {
|
||||
const onClose = jest.fn();
|
||||
render(<ChangelogModal onClose={onClose} />);
|
||||
fireEvent.click(screen.getByText('Skip for now'));
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens migration docs when Update my workspace is clicked', () => {
|
||||
window.open = jest.fn();
|
||||
render(<ChangelogModal onClose={jest.fn()} />);
|
||||
fireEvent.click(screen.getByText('Update my workspace'));
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://github.com/SigNoz/signoz/releases',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
});
|
||||
|
||||
it('scrolls for more when Scroll for more is clicked', () => {
|
||||
render(<ChangelogModal onClose={jest.fn()} />);
|
||||
const scrollBtn = screen.getByTestId('scroll-more-btn');
|
||||
const contentDiv = screen.getByTestId('changelog-content');
|
||||
if (contentDiv) {
|
||||
contentDiv.scrollTo = jest.fn();
|
||||
}
|
||||
fireEvent.click(scrollBtn);
|
||||
if (contentDiv) {
|
||||
expect(contentDiv.scrollTo).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import ChangelogRenderer from '../components/ChangelogRenderer';
|
||||
|
||||
// Mock react-markdown to just render children as plain text
|
||||
jest.mock(
|
||||
'react-markdown',
|
||||
() =>
|
||||
function ReactMarkdown({ children }: any) {
|
||||
return <div>{children}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
const mockChangelog = {
|
||||
id: 1,
|
||||
documentId: 'changelog-doc-1',
|
||||
version: '1.0.0',
|
||||
createdAt: '2025-06-09T12:00:00Z',
|
||||
release_date: '2025-06-10',
|
||||
features: [
|
||||
{
|
||||
id: 1,
|
||||
documentId: '1',
|
||||
title: 'Feature 1',
|
||||
description: 'Description for feature 1',
|
||||
sort_order: 1,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
publishedAt: '',
|
||||
deployment_type: 'All',
|
||||
media: {
|
||||
id: 1,
|
||||
documentId: 'doc1',
|
||||
ext: '.webp',
|
||||
url: '/uploads/feature1.webp',
|
||||
mime: 'image/webp',
|
||||
alternativeText: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
bug_fixes: 'Bug fix details',
|
||||
updatedAt: '2025-06-09T12:00:00Z',
|
||||
publishedAt: '2025-06-09T12:00:00Z',
|
||||
maintenance: 'Maintenance details',
|
||||
};
|
||||
|
||||
describe('ChangelogRenderer', () => {
|
||||
it('renders release date', () => {
|
||||
render(<ChangelogRenderer changelog={mockChangelog} />);
|
||||
expect(screen.getByText('June 10, 2025')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders features, media, and description', () => {
|
||||
render(<ChangelogRenderer changelog={mockChangelog} />);
|
||||
expect(screen.getByText('Feature 1')).toBeInTheDocument();
|
||||
expect(screen.getByAltText('Media')).toBeInTheDocument();
|
||||
expect(screen.getByText('Description for feature 1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
.changelog-renderer {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
|
||||
.changelog-release-date {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
&-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
&-line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
bottom: -30px;
|
||||
width: 1px;
|
||||
background-color: var(--bg-slate-400, #1d212d);
|
||||
|
||||
.inner-ball {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--bg-robin-500, #7190f9);
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-left: 30px;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 10px;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: var(--bg-robin-500, #7190f9);
|
||||
transform: translate(-100%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li,
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 4px;
|
||||
background-color: var(--bg-slate-500, #161922);
|
||||
border-radius: 6px;
|
||||
font-size: 95%;
|
||||
vertical-align: middle;
|
||||
border: 1px solid var(--bg-slate-600, #1c1f2a);
|
||||
}
|
||||
a {
|
||||
color: var(--text-robin-500, #7190f9);
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
color: var(--text-vanilla-100, #fff);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.changelog-media-image,
|
||||
.changelog-media-video {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
|
||||
.changelog-media-video {
|
||||
margin: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.changelog-renderer {
|
||||
.changelog-release-date {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
&-line {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
li,
|
||||
p {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import './ChangelogRenderer.styles.scss';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import {
|
||||
ChangelogSchema,
|
||||
Media,
|
||||
SupportedImageTypes,
|
||||
SupportedVideoTypes,
|
||||
} from 'types/api/changelog/getChangelogByVersion';
|
||||
|
||||
interface Props {
|
||||
changelog: ChangelogSchema;
|
||||
}
|
||||
|
||||
function renderMedia(media: Media): JSX.Element | null {
|
||||
if (SupportedImageTypes.includes(media.ext)) {
|
||||
return (
|
||||
<img
|
||||
src={media.url}
|
||||
alt={media.alternativeText || 'Media'}
|
||||
width={800}
|
||||
height={450}
|
||||
className="changelog-media-image"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (SupportedVideoTypes.includes(media.ext)) {
|
||||
return (
|
||||
<video
|
||||
autoPlay
|
||||
controls
|
||||
controlsList="nodownload noplaybackrate"
|
||||
loop
|
||||
className="changelog-media-video"
|
||||
>
|
||||
<source src={media.url} type={media.mime} />
|
||||
<track kind="captions" src="" label="No captions available" default />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function ChangelogRenderer({ changelog }: Props): JSX.Element {
|
||||
const formattedReleaseDate = dayjs(changelog.release_date).format(
|
||||
'MMMM D, YYYY',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="changelog-renderer">
|
||||
<div className="changelog-renderer-line">
|
||||
<div className="inner-ball" />
|
||||
</div>
|
||||
<span className="changelog-release-date">{formattedReleaseDate}</span>
|
||||
{changelog.features && changelog.features.length > 0 && (
|
||||
<div className="changelog-renderer-list">
|
||||
{changelog.features.map((feature) => (
|
||||
<div key={feature.id}>
|
||||
<h2>{feature.title}</h2>
|
||||
{feature.media && renderMedia(feature.media)}
|
||||
<ReactMarkdown>{feature.description}</ReactMarkdown>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{changelog.bug_fixes && changelog.bug_fixes.length > 0 && (
|
||||
<div>
|
||||
<h2>Bug Fixes</h2>
|
||||
{changelog.bug_fixes && (
|
||||
<ReactMarkdown>{changelog.bug_fixes}</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{changelog.maintenance && changelog.maintenance.length > 0 && (
|
||||
<div>
|
||||
<h2>Maintenance</h2>
|
||||
{changelog.maintenance && (
|
||||
<ReactMarkdown>{changelog.maintenance}</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangelogRenderer;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -52,11 +53,32 @@ jest.mock('hooks/saveViews/useDeleteView', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock usePreferenceSync
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ExplorerCard', () => {
|
||||
it('renders a card with a title and a description', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
|
||||
@@ -65,7 +87,9 @@ describe('ExplorerCard', () => {
|
||||
it('renders a save view button', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
DeleteViewHandlerProps,
|
||||
@@ -106,7 +107,11 @@ export const isQueryUpdatedInView = ({
|
||||
!isEqual(
|
||||
options?.selectColumns,
|
||||
extraData && JSON.parse(extraData)?.selectColumns,
|
||||
)
|
||||
) ||
|
||||
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
|
||||
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
|
||||
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
|
||||
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ const formatMap = {
|
||||
'MM/dd HH:mm': DATE_TIME_FORMATS.SLASH_SHORT,
|
||||
'MM/DD': DATE_TIME_FORMATS.DATE_SHORT,
|
||||
'YY-MM': DATE_TIME_FORMATS.YEAR_MONTH,
|
||||
'MMM d, yyyy, h:mm:ss aaaa': DATE_TIME_FORMATS.DASH_DATETIME,
|
||||
YY: DATE_TIME_FORMATS.YEAR_SHORT,
|
||||
};
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ function HostMetricTraces({
|
||||
{!isError && traces.length > 0 && (
|
||||
<div className="host-metric-traces-table">
|
||||
<TraceExplorerControls
|
||||
isLoading={isFetching}
|
||||
isLoading={isFetching && traces.length === 0}
|
||||
totalCount={totalCount}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
showSizeChanger={false}
|
||||
@@ -203,7 +203,7 @@ function HostMetricTraces({
|
||||
tableLayout="fixed"
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
loading={isFetching}
|
||||
loading={isFetching && traces.length === 0}
|
||||
dataSource={traces}
|
||||
columns={traceListColumns}
|
||||
onRow={(): Record<string, unknown> => ({
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -86,8 +86,12 @@ function HostMetricsDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(
|
||||
@@ -150,10 +154,11 @@ function HostMetricsDetails({
|
||||
}, [initialFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -181,6 +186,7 @@ function HostMetricsDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -356,6 +362,7 @@ function HostMetricsDetails({
|
||||
|
||||
const handleClose = (): void => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
lastSelectedInterval.current = null;
|
||||
setSearchParams({});
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
@@ -430,9 +437,13 @@ function HostMetricsDetails({
|
||||
>
|
||||
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
||||
</Tag>
|
||||
<Tag className="infra-monitoring-tags" bordered>
|
||||
{host.os}
|
||||
</Tag>
|
||||
{host.os ? (
|
||||
<Tag className="infra-monitoring-tags" bordered>
|
||||
{host.os}
|
||||
</Tag>
|
||||
) : (
|
||||
<Typography.Text>-</Typography.Text>
|
||||
)}
|
||||
<div className="progress-container">
|
||||
<Progress
|
||||
percent={Number((host.cpu * 100).toFixed(1))}
|
||||
|
||||
@@ -15,11 +15,12 @@ import {
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useMultiIntersectionObserver } from 'hooks/useMultiIntersectionObserver';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueries, UseQueryResult } from 'react-query';
|
||||
import { QueryFunctionContext, useQueries, UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
@@ -53,6 +54,11 @@ function Metrics({
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
visibilities,
|
||||
setElement,
|
||||
} = useMultiIntersectionObserver(hostWidgetInfo.length, { threshold: 0.1 });
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() =>
|
||||
getHostQueryPayload(
|
||||
@@ -65,11 +71,15 @@ function Metrics({
|
||||
);
|
||||
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryPayloads.map((payload, index) => ({
|
||||
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
queryFn: ({
|
||||
signal,
|
||||
}: QueryFunctionContext): Promise<
|
||||
SuccessResponse<MetricRangePayloadProps>
|
||||
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
|
||||
enabled: !!payload && visibilities[index],
|
||||
keepPreviousData: true,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -143,7 +153,7 @@ function Metrics({
|
||||
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||
idx: number,
|
||||
): JSX.Element => {
|
||||
if (query.isLoading) {
|
||||
if ((!query.data && query.isLoading) || !visibilities[idx]) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
@@ -181,7 +191,7 @@ function Metrics({
|
||||
</div>
|
||||
<Row gutter={24} className="host-metrics-container">
|
||||
{queries.map((query, idx) => (
|
||||
<Col span={12} key={hostWidgetInfo[idx].title}>
|
||||
<Col ref={setElement(idx)} span={12} key={hostWidgetInfo[idx].title}>
|
||||
<Typography.Text>{hostWidgetInfo[idx].title}</Typography.Text>
|
||||
<Card bordered className="host-metrics-card" ref={graphRef}>
|
||||
{renderCardContent(query, idx)}
|
||||
|
||||
@@ -71,7 +71,7 @@ function LogDetail({
|
||||
const [contextQuery, setContextQuery] = useState<Query | undefined>();
|
||||
const [filters, setFilters] = useState<TagFilter | null>(null);
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
const { initialDataSource, stagedQuery } = useQueryBuilder();
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
@@ -81,7 +81,7 @@ function LogDetail({
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: initialDataSource || DataSource.LOGS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,17 +5,19 @@ import cx from 'classnames';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { memo, MouseEvent, ReactNode, useMemo } from 'react';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
function AddToQueryHOC({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
onAddToQuery,
|
||||
fontSize,
|
||||
dataType = DataTypes.EMPTY,
|
||||
children,
|
||||
}: AddToQueryHOCProps): JSX.Element {
|
||||
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
|
||||
event.stopPropagation();
|
||||
onAddToQuery(fieldKey, fieldValue, OPERATORS['=']);
|
||||
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], undefined, dataType);
|
||||
};
|
||||
|
||||
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
||||
@@ -35,9 +37,20 @@ function AddToQueryHOC({
|
||||
export interface AddToQueryHOCProps {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
|
||||
onAddToQuery: (
|
||||
fieldKey: string,
|
||||
fieldValue: string,
|
||||
operator: string,
|
||||
isJSON?: boolean,
|
||||
dataType?: DataTypes,
|
||||
) => void;
|
||||
fontSize: FontSize;
|
||||
dataType?: DataTypes;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
AddToQueryHOC.defaultProps = {
|
||||
dataType: DataTypes.EMPTY,
|
||||
};
|
||||
|
||||
export default memo(AddToQueryHOC);
|
||||
|
||||
@@ -20,6 +20,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
||||
|
||||
export const defaultTableStyle: CSSProperties = {
|
||||
minWidth: '40rem',
|
||||
maxWidth: '60rem',
|
||||
};
|
||||
|
||||
export const defaultListViewPanelStyle: CSSProperties = {
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import {
|
||||
generatePath,
|
||||
matchPath,
|
||||
useLocation,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { RouteTabProps } from './types';
|
||||
|
||||
interface Params {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
function RouteTab({
|
||||
routes,
|
||||
activeKey,
|
||||
@@ -9,6 +19,18 @@ function RouteTab({
|
||||
history,
|
||||
...rest
|
||||
}: RouteTabProps & TabsProps): JSX.Element {
|
||||
const params = useParams<Params>();
|
||||
const location = useLocation();
|
||||
|
||||
// Find the matching route for the current pathname
|
||||
const currentRoute = routes.find((route) => {
|
||||
const routePath = route.route.split('?')[0];
|
||||
return matchPath(location.pathname, {
|
||||
path: routePath,
|
||||
exact: true,
|
||||
});
|
||||
});
|
||||
|
||||
const onChange = (activeRoute: string): void => {
|
||||
if (onChangeHandler) {
|
||||
onChangeHandler(activeRoute);
|
||||
@@ -17,7 +39,8 @@ function RouteTab({
|
||||
const selectedRoute = routes.find((e) => e.key === activeRoute);
|
||||
|
||||
if (selectedRoute) {
|
||||
history.push(selectedRoute.route);
|
||||
const resolvedRoute = generatePath(selectedRoute.route, params);
|
||||
history.push(resolvedRoute);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,8 +55,8 @@ function RouteTab({
|
||||
<Tabs
|
||||
onChange={onChange}
|
||||
destroyInactiveTabPane
|
||||
activeKey={activeKey}
|
||||
defaultActiveKey={activeKey}
|
||||
activeKey={currentRoute?.key || activeKey}
|
||||
defaultActiveKey={currentRoute?.key || activeKey}
|
||||
animated
|
||||
items={items}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
|
||||
@@ -30,5 +30,5 @@ export enum LOCALSTORAGE {
|
||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||
BANNER_DISMISSED = 'BANNER_DISMISSED',
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
UNEXECUTED_FUNNELS = 'UNEXECUTED_FUNNELS',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
}
|
||||
|
||||
18
frontend/src/constants/orgPreferences.ts
Normal file
18
frontend/src/constants/orgPreferences.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const ORG_PREFERENCES = {
|
||||
ORG_ONBOARDING: 'org_onboarding',
|
||||
WELCOME_CHECKLIST_DO_LATER: 'welcome_checklist_do_later',
|
||||
WELCOME_CHECKLIST_SEND_LOGS_SKIPPED: 'welcome_checklist_send_logs_skipped',
|
||||
WELCOME_CHECKLIST_SEND_TRACES_SKIPPED: 'welcome_checklist_send_traces_skipped',
|
||||
WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED:
|
||||
'welcome_checklist_setup_alerts_skipped',
|
||||
WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED:
|
||||
'welcome_checklist_setup_saved_view_skipped',
|
||||
WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED:
|
||||
'welcome_checklist_send_infra_metrics_skipped',
|
||||
WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED:
|
||||
'welcome_checklist_setup_dashboards_skipped',
|
||||
WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED:
|
||||
'welcome_checklist_setup_workspace_skipped',
|
||||
WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED:
|
||||
'welcome_checklist_add_data_source_skipped',
|
||||
};
|
||||
@@ -46,4 +46,5 @@ export enum QueryParams {
|
||||
msgSystem = 'msgSystem',
|
||||
destination = 'destination',
|
||||
kindString = 'kindString',
|
||||
tab = 'tab',
|
||||
}
|
||||
|
||||
@@ -425,3 +425,79 @@ export const metricsEmptyTimeAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [];
|
||||
|
||||
export const metricsUnknownTimeAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.MAX,
|
||||
label: 'Max',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MIN,
|
||||
label: 'Min',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.SUM,
|
||||
label: 'Sum',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.AVG,
|
||||
label: 'Avg',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.COUNT,
|
||||
label: 'Count',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.RATE,
|
||||
label: 'Rate',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.INCREASE,
|
||||
label: 'Increase',
|
||||
},
|
||||
];
|
||||
|
||||
export const metricsUnknownSpaceAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.SUM,
|
||||
label: 'Sum',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.AVG,
|
||||
label: 'Avg',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MIN,
|
||||
label: 'Min',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MAX,
|
||||
label: 'Max',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P50,
|
||||
label: 'P50',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P75,
|
||||
label: 'P75',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P90,
|
||||
label: 'P90',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P95,
|
||||
label: 'P95',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P99,
|
||||
label: 'P99',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -29,12 +29,12 @@ const ROUTES = {
|
||||
ALERT_OVERVIEW: '/alerts/overview',
|
||||
ALL_CHANNELS: '/settings/channels',
|
||||
CHANNELS_NEW: '/settings/channels/new',
|
||||
CHANNELS_EDIT: '/settings/channels/:id',
|
||||
CHANNELS_EDIT: '/settings/channels/edit/:channelId',
|
||||
ALL_ERROR: '/exceptions',
|
||||
ERROR_DETAIL: '/error-detail',
|
||||
VERSION: '/status',
|
||||
MY_SETTINGS: '/my-settings',
|
||||
SETTINGS: '/settings',
|
||||
MY_SETTINGS: '/settings/my-settings',
|
||||
ORG_SETTINGS: '/settings/org-settings',
|
||||
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
|
||||
API_KEYS: '/settings/api-keys',
|
||||
@@ -52,7 +52,7 @@ const ROUTES = {
|
||||
LIST_LICENSES: '/licenses',
|
||||
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
|
||||
TRACE_EXPLORER: '/trace-explorer',
|
||||
BILLING: '/billing',
|
||||
BILLING: '/settings/billing',
|
||||
SUPPORT: '/support',
|
||||
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
||||
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
||||
@@ -60,10 +60,12 @@ const ROUTES = {
|
||||
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
|
||||
WORKSPACE_LOCKED: '/workspace-locked',
|
||||
WORKSPACE_SUSPENDED: '/workspace-suspended',
|
||||
SHORTCUTS: '/shortcuts',
|
||||
SHORTCUTS: '/settings/shortcuts',
|
||||
INTEGRATIONS: '/integrations',
|
||||
MESSAGING_QUEUES_BASE: '/messaging-queues',
|
||||
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
|
||||
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',
|
||||
INFRASTRUCTURE_MONITORING_BASE: '/infrastructure-monitoring',
|
||||
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
|
||||
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
|
||||
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
|
||||
@@ -71,6 +73,7 @@ const ROUTES = {
|
||||
METRICS_EXPLORER: '/metrics-explorer/summary',
|
||||
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
||||
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
||||
API_MONITORING_BASE: '/api-monitoring',
|
||||
API_MONITORING: '/api-monitoring/explorer',
|
||||
METRICS_EXPLORER_BASE: '/metrics-explorer',
|
||||
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
||||
|
||||
4
frontend/src/constants/userPreferences.ts
Normal file
4
frontend/src/constants/userPreferences.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const USER_PREFERENCES = {
|
||||
SIDENAV_PINNED: 'sidenav_pinned',
|
||||
NAV_SHORTCUTS: 'nav_shortcuts',
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
@@ -17,14 +17,13 @@ import Delete from './Delete';
|
||||
function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
const { t } = useTranslation(['channels']);
|
||||
const { notifications } = useNotifications();
|
||||
const [channels, setChannels] = useState<Channels[]>(allChannels);
|
||||
const { user } = useAppContext();
|
||||
const [action] = useComponentPermission(['new_alert_action'], user.role);
|
||||
|
||||
const onClickEditHandler = useCallback((id: string) => {
|
||||
history.replace(
|
||||
history.push(
|
||||
generatePath(ROUTES.CHANNELS_EDIT, {
|
||||
id,
|
||||
channelId: id,
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
@@ -56,14 +55,19 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
<Button onClick={(): void => onClickEditHandler(id)} type="link">
|
||||
{t('column_channel_edit')}
|
||||
</Button>
|
||||
<Delete id={id} setChannels={setChannels} notifications={notifications} />
|
||||
<Delete id={id} notifications={notifications} />
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizeTable columns={columns} dataSource={channels} rowKey="id" bordered />
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={allChannels}
|
||||
rowKey="id"
|
||||
bordered
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.alert-channels-container {
|
||||
width: 90%;
|
||||
margin: 12px auto;
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Button } from 'antd';
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import deleteChannel from 'api/channels/delete';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
|
||||
function Delete({ notifications, id }: DeleteProps): JSX.Element {
|
||||
const { t } = useTranslation(['channels']);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const onClickHandler = async (): Promise<void> => {
|
||||
try {
|
||||
@@ -21,7 +22,8 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
|
||||
message: 'Success',
|
||||
description: t('channel_delete_success'),
|
||||
});
|
||||
setChannels((preChannels) => preChannels.filter((e) => e.id !== id));
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries(['getChannels']);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
@@ -46,7 +48,6 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
|
||||
|
||||
interface DeleteProps {
|
||||
notifications: NotificationInstance;
|
||||
setChannels: Dispatch<SetStateAction<Channels[]>>;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,14 +120,19 @@ describe('Create Alert Channel', () => {
|
||||
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
|
||||
expect(screen.getByText('button_return')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if saving the form without filling the name displays "Something went wrong"', async () => {
|
||||
it('Should check if saving the form without filling the name displays error notification', async () => {
|
||||
const saveButton = screen.getByRole('button', {
|
||||
name: 'button_save_channel',
|
||||
});
|
||||
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => expect(showErrorModal).toHaveBeenCalled());
|
||||
await waitFor(() =>
|
||||
expect(errorNotification).toHaveBeenCalledWith({
|
||||
message: 'Error',
|
||||
description: 'channel_name_required',
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('Should check if clicking on Test button shows "An alert has been sent to this channel" success message if testing passes', async () => {
|
||||
server.use(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './AllAlertChannels.styles.scss';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import getAll from 'api/channels/getAll';
|
||||
@@ -56,7 +58,7 @@ function AlertChannels(): JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="alert-channels-container">
|
||||
<ButtonContainer>
|
||||
<Paragraph ellipsis type="secondary">
|
||||
{t('sending_channels_note')}
|
||||
@@ -87,7 +89,7 @@ function AlertChannels(): JSX.Element {
|
||||
</ButtonContainer>
|
||||
|
||||
<AlertChannelsComponent allChannels={data?.data || []} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,37 @@
|
||||
// Earlier we were having app-banner-container class
|
||||
// we change it to app-banner-wrapper as the adblocker was blocking the app-banner-container class
|
||||
// Keep an eye on What classnames are used in the codebase
|
||||
.app-banner-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.isWorkspaceRestricted {
|
||||
height: calc(100% - 32px);
|
||||
|
||||
// same styles as its either trial expired or payment failed
|
||||
&.isTrialExpired {
|
||||
height: calc(100% - 64px);
|
||||
}
|
||||
|
||||
&.isPaymentFailed {
|
||||
height: calc(100% - 64px);
|
||||
}
|
||||
}
|
||||
|
||||
&.isTrialExpired {
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
|
||||
&.isPaymentFailed {
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.app-content {
|
||||
width: calc(100% - 64px); // width of the sidebar
|
||||
z-index: 0;
|
||||
@@ -22,6 +51,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.side-nav-pinned {
|
||||
.app-content {
|
||||
width: calc(100% - 240px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-support-gateway {
|
||||
@@ -157,3 +192,9 @@
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-restricted-banner,
|
||||
.trial-expiry-banner,
|
||||
.payment-failed-banner {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as Sentry from '@sentry/react';
|
||||
import { Flex } from 'antd';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import manageCreditCardApi from 'api/v1/portal/create';
|
||||
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
||||
@@ -18,6 +19,7 @@ import { Events } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { USER_PREFERENCES } from 'constants/userPreferences';
|
||||
import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -27,7 +29,6 @@ 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,
|
||||
@@ -40,9 +41,10 @@ import {
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQueries } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
UPDATE_CURRENT_ERROR,
|
||||
@@ -50,14 +52,16 @@ import {
|
||||
UPDATE_LATEST_VERSION,
|
||||
UPDATE_LATEST_VERSION_ERROR,
|
||||
} from 'types/actions/app';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||
import APIError from 'types/api/error';
|
||||
import {
|
||||
LicenseEvent,
|
||||
LicensePlatform,
|
||||
LicenseState,
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import {
|
||||
@@ -80,6 +84,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
featureFlags,
|
||||
isFetchingFeatureFlags,
|
||||
featureFlagsFetchError,
|
||||
userPreferences,
|
||||
updateChangelog,
|
||||
} = useAppContext();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
@@ -92,6 +98,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
||||
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||
|
||||
const { latestVersion } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const handleBillingOnSuccess = (
|
||||
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||
): void => {
|
||||
@@ -128,7 +138,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
|
||||
const [
|
||||
getUserVersionResponse,
|
||||
getUserLatestVersionResponse,
|
||||
getChangelogByVersionResponse,
|
||||
] = useQueries([
|
||||
{
|
||||
queryFn: getUserVersion,
|
||||
queryKey: ['getUserVersion', user?.accessJwt],
|
||||
@@ -139,6 +153,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
queryKey: ['getUserLatestVersion', user?.accessJwt],
|
||||
enabled: isLoggedIn,
|
||||
},
|
||||
{
|
||||
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
|
||||
getChangelogByVersion(latestVersion),
|
||||
queryKey: ['getChangelogByVersion', latestVersion],
|
||||
enabled: isLoggedIn && !isCloudUserVal && Boolean(latestVersion),
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -197,7 +217,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
if (
|
||||
getUserVersionResponse.isFetched &&
|
||||
getUserLatestVersionResponse.isSuccess &&
|
||||
getUserVersionResponse.isSuccess &&
|
||||
getUserVersionResponse.data &&
|
||||
getUserVersionResponse.data.payload
|
||||
) {
|
||||
@@ -235,12 +255,31 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
getUserVersionResponse.isLoading,
|
||||
getUserVersionResponse.isError,
|
||||
getUserVersionResponse.data,
|
||||
getUserVersionResponse.isSuccess,
|
||||
getUserLatestVersionResponse.isFetched,
|
||||
getUserVersionResponse.isFetched,
|
||||
getUserLatestVersionResponse.isSuccess,
|
||||
notifications,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getChangelogByVersionResponse.isFetched &&
|
||||
getChangelogByVersionResponse.isSuccess &&
|
||||
getChangelogByVersionResponse.data &&
|
||||
getChangelogByVersionResponse.data.payload
|
||||
) {
|
||||
updateChangelog(getChangelogByVersionResponse.data.payload);
|
||||
}
|
||||
}, [
|
||||
updateChangelog,
|
||||
getChangelogByVersionResponse.isFetched,
|
||||
getChangelogByVersionResponse.isLoading,
|
||||
getChangelogByVersionResponse.isError,
|
||||
getChangelogByVersionResponse.data,
|
||||
getChangelogByVersionResponse.isSuccess,
|
||||
]);
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
|
||||
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
||||
@@ -330,53 +369,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
});
|
||||
}, [manageCreditCard]);
|
||||
|
||||
const isHome = (): boolean => routeKey === 'HOME';
|
||||
|
||||
const isLogsView = (): boolean =>
|
||||
routeKey === 'LOGS' ||
|
||||
routeKey === 'LOGS_EXPLORER' ||
|
||||
routeKey === 'LOGS_PIPELINES' ||
|
||||
routeKey === 'LOGS_SAVE_VIEWS';
|
||||
|
||||
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
|
||||
|
||||
const isExceptionsView = (): boolean => routeKey === 'ALL_ERROR';
|
||||
|
||||
const isTracesView = (): boolean =>
|
||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||
|
||||
const isMessagingQueues = (): boolean =>
|
||||
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';
|
||||
const isInfraMonitoring = (): boolean =>
|
||||
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS' ||
|
||||
routeKey === 'INFRASTRUCTURE_MONITORING_KUBERNETES';
|
||||
const isTracesFunnels = (): boolean => routeKey === 'TRACES_FUNNELS';
|
||||
const isTracesFunnelDetails = (): boolean =>
|
||||
!!matchPath(pathname, ROUTES.TRACES_FUNNELS_DETAIL);
|
||||
|
||||
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
|
||||
|
||||
const isDashboardView = (): boolean =>
|
||||
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+$/);
|
||||
|
||||
const isDashboardWidgetView = (): boolean =>
|
||||
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+\/new$/);
|
||||
|
||||
const isTraceDetailsView = (): boolean =>
|
||||
isPathMatch(/^\/trace\/[a-zA-Z0-9]+(\?.*)?$/);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDarkMode) {
|
||||
document.body.classList.remove('lightMode');
|
||||
@@ -593,61 +585,84 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
</div>
|
||||
);
|
||||
|
||||
const sideNavPinned = userPreferences?.find(
|
||||
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
|
||||
)?.value as boolean;
|
||||
|
||||
const SHOW_TRIAL_EXPIRY_BANNER =
|
||||
showTrialExpiryBanner && !showPaymentFailedWarning;
|
||||
const SHOW_WORKSPACE_RESTRICTED_BANNER = showWorkspaceRestricted;
|
||||
const SHOW_PAYMENT_FAILED_BANNER =
|
||||
!showTrialExpiryBanner && showPaymentFailedWarning;
|
||||
|
||||
return (
|
||||
<Layout className={cx(isDarkMode ? 'darkMode dark' : 'lightMode')}>
|
||||
<Helmet>
|
||||
<title>{pageTitle}</title>
|
||||
</Helmet>
|
||||
|
||||
{showTrialExpiryBanner && !showPaymentFailedWarning && (
|
||||
<div className="trial-expiry-banner">
|
||||
You are in free trial period. Your free trial will end on{' '}
|
||||
<span>{getFormattedDate(trialInfo?.trialEnd || Date.now())}.</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||
upgrade
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
'Please contact your administrator for upgrading to a paid plan.'
|
||||
{isLoggedIn && (
|
||||
<div className={cx('app-banner-wrapper')}>
|
||||
{SHOW_TRIAL_EXPIRY_BANNER && (
|
||||
<div className="trial-expiry-banner">
|
||||
You are in free trial period. Your free trial will end on{' '}
|
||||
<span>{getFormattedDate(trialInfo?.trialEnd || Date.now())}.</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||
upgrade
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
'Please contact your administrator for upgrading to a paid plan.'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showWorkspaceRestricted && renderWorkspaceRestrictedBanner()}
|
||||
{SHOW_WORKSPACE_RESTRICTED_BANNER && renderWorkspaceRestrictedBanner()}
|
||||
|
||||
{!showTrialExpiryBanner && showPaymentFailedWarning && (
|
||||
<div className="payment-failed-banner">
|
||||
Your bill payment has failed. Your workspace will get suspended on{' '}
|
||||
<span>
|
||||
{getFormattedDateWithMinutes(
|
||||
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
|
||||
)}
|
||||
.
|
||||
</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleFailedPayment}>
|
||||
pay the bill
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
' Please contact your administrator to pay the bill.'
|
||||
{SHOW_PAYMENT_FAILED_BANNER && (
|
||||
<div className="payment-failed-banner">
|
||||
Your bill payment has failed. Your workspace will get suspended on{' '}
|
||||
<span>
|
||||
{getFormattedDateWithMinutes(
|
||||
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
|
||||
)}
|
||||
.
|
||||
</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleFailedPayment}>
|
||||
pay the bill
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
' Please contact your administrator to pay the bill.'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
className={cx('app-layout', isDarkMode ? 'darkMode dark' : 'lightMode')}
|
||||
className={cx(
|
||||
'app-layout',
|
||||
isDarkMode ? 'darkMode dark' : 'lightMode',
|
||||
sideNavPinned ? 'side-nav-pinned' : '',
|
||||
SHOW_WORKSPACE_RESTRICTED_BANNER ? 'isWorkspaceRestricted' : '',
|
||||
SHOW_TRIAL_EXPIRY_BANNER ? 'isTrialExpired' : '',
|
||||
SHOW_PAYMENT_FAILED_BANNER ? 'isPaymentFailed' : '',
|
||||
)}
|
||||
>
|
||||
{isToDisplayLayout && !renderFullScreen && <SideNav />}
|
||||
{isToDisplayLayout && !renderFullScreen && (
|
||||
<SideNav isPinned={sideNavPinned} />
|
||||
)}
|
||||
<div
|
||||
className={cx('app-content', {
|
||||
'full-screen-content': renderFullScreen,
|
||||
@@ -657,32 +672,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<LayoutContent data-overlayscrollbars-initialize>
|
||||
<OverlayScrollbar>
|
||||
<ChildrenContainer
|
||||
style={{
|
||||
margin:
|
||||
isHome() ||
|
||||
isLogsView() ||
|
||||
isTracesView() ||
|
||||
isDashboardView() ||
|
||||
isDashboardWidgetView() ||
|
||||
isDashboardListView() ||
|
||||
isAlertHistory() ||
|
||||
isAlertOverview() ||
|
||||
isMessagingQueues() ||
|
||||
isCloudIntegrationPage() ||
|
||||
isInfraMonitoring() ||
|
||||
isApiMonitoringView() ||
|
||||
isExceptionsView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
|
||||
...(isTraceDetailsView() ||
|
||||
isTracesFunnels() ||
|
||||
isTracesFunnelDetails()
|
||||
? { margin: 0 }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<ChildrenContainer>
|
||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
.billing-container {
|
||||
margin-bottom: 40px;
|
||||
padding-top: 36px;
|
||||
width: 65%;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
|
||||
.billing-summary {
|
||||
margin: 24px 8px;
|
||||
|
||||
@@ -14,7 +14,7 @@ function CloudIntegrationPage(): JSX.Element {
|
||||
<HeroSection />
|
||||
<RequestIntegrationBtn
|
||||
type={IntegrationType.AWS_SERVICES}
|
||||
message="Cannot find the AWS service you're looking for? Request more integrations"
|
||||
message="Can't find the AWS service you're looking for? Request more integrations"
|
||||
/>
|
||||
<ServicesTabs />
|
||||
</div>
|
||||
|
||||
@@ -60,26 +60,30 @@ function CloudServiceDataCollected({
|
||||
|
||||
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>
|
||||
{logsData && logsData.length > 0 && (
|
||||
<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>
|
||||
)}
|
||||
{metricsData && metricsData.length > 0 && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,33 @@ function ServiceStatus({
|
||||
return <div className={`service-status ${className}`}>{text}</div>;
|
||||
}
|
||||
|
||||
function getTabItems(serviceDetailsData: any): TabsProps['items'] {
|
||||
const dashboards = serviceDetailsData?.assets.dashboards || [];
|
||||
const dataCollected = serviceDetailsData?.data_collected || {};
|
||||
const items: TabsProps['items'] = [];
|
||||
|
||||
if (dashboards.length) {
|
||||
items.push({
|
||||
key: 'dashboards',
|
||||
label: `Dashboards (${dashboards.length})`,
|
||||
children: <CloudServiceDashboards service={serviceDetailsData} />,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: 'data-collected',
|
||||
label: 'Data Collected',
|
||||
children: (
|
||||
<CloudServiceDataCollected
|
||||
logsData={dataCollected.logs || []}
|
||||
metricsData={dataCollected.metrics || []}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function ServiceDetails(): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId');
|
||||
@@ -106,23 +133,7 @@ function ServiceDetails(): JSX.Element | null {
|
||||
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 || []}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
const tabItems = getTabItems(serviceDetailsData);
|
||||
|
||||
return (
|
||||
<div className="service-details">
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('Request AWS integration', () => {
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
/cannot find what you’re looking for\? request more integrations/i,
|
||||
/can't find what you’re looking for\? request more integrations/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.create-alert-channels-container {
|
||||
width: 90%;
|
||||
margin: 12px auto;
|
||||
|
||||
border: 1px solid var(--Slate-500, #161922);
|
||||
background: var(--Ink-400, #121317);
|
||||
border-radius: 3px;
|
||||
|
||||
padding: 16px;
|
||||
|
||||
.form-alert-channels-title {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import './CreateAlertChannels.styles.scss';
|
||||
|
||||
import { Form } from 'antd';
|
||||
import createEmail from 'api/channels/createEmail';
|
||||
import createMsTeamsApi from 'api/channels/createMsTeams';
|
||||
@@ -136,6 +138,14 @@ function CreateAlertChannels({
|
||||
);
|
||||
|
||||
const onSlackHandler = useCallback(async () => {
|
||||
if (!selectedConfig.api_url) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('webhook_url_required'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingState(true);
|
||||
|
||||
try {
|
||||
@@ -152,7 +162,7 @@ function CreateAlertChannels({
|
||||
} finally {
|
||||
setSavingState(false);
|
||||
}
|
||||
}, [prepareSlackRequest, notifications, t, showErrorModal]);
|
||||
}, [selectedConfig, notifications, t, prepareSlackRequest, showErrorModal]);
|
||||
|
||||
const prepareWebhookRequest = useCallback(() => {
|
||||
// initial api request without auth params
|
||||
@@ -190,6 +200,14 @@ function CreateAlertChannels({
|
||||
}, [notifications, t, selectedConfig]);
|
||||
|
||||
const onWebhookHandler = useCallback(async () => {
|
||||
if (!selectedConfig.api_url) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('webhook_url_required'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingState(true);
|
||||
try {
|
||||
const request = prepareWebhookRequest();
|
||||
@@ -206,7 +224,13 @@ function CreateAlertChannels({
|
||||
} finally {
|
||||
setSavingState(false);
|
||||
}
|
||||
}, [prepareWebhookRequest, notifications, t, showErrorModal]);
|
||||
}, [
|
||||
selectedConfig.api_url,
|
||||
notifications,
|
||||
t,
|
||||
prepareWebhookRequest,
|
||||
showErrorModal,
|
||||
]);
|
||||
|
||||
const preparePagerRequest = useCallback(() => {
|
||||
const validationError = ValidatePagerChannel(selectedConfig as PagerChannel);
|
||||
@@ -270,6 +294,14 @@ function CreateAlertChannels({
|
||||
);
|
||||
|
||||
const onOpsgenieHandler = useCallback(async () => {
|
||||
if (!selectedConfig.api_key) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('api_key_required'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingState(true);
|
||||
try {
|
||||
await createOpsgenie(prepareOpsgenieRequest());
|
||||
@@ -285,7 +317,13 @@ function CreateAlertChannels({
|
||||
} finally {
|
||||
setSavingState(false);
|
||||
}
|
||||
}, [prepareOpsgenieRequest, notifications, t, showErrorModal]);
|
||||
}, [
|
||||
selectedConfig.api_key,
|
||||
notifications,
|
||||
t,
|
||||
prepareOpsgenieRequest,
|
||||
showErrorModal,
|
||||
]);
|
||||
|
||||
const prepareEmailRequest = useCallback(
|
||||
() => ({
|
||||
@@ -299,6 +337,14 @@ function CreateAlertChannels({
|
||||
);
|
||||
|
||||
const onEmailHandler = useCallback(async () => {
|
||||
if (!selectedConfig.to) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('to_required'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingState(true);
|
||||
try {
|
||||
const request = prepareEmailRequest();
|
||||
@@ -315,7 +361,7 @@ function CreateAlertChannels({
|
||||
} finally {
|
||||
setSavingState(false);
|
||||
}
|
||||
}, [prepareEmailRequest, notifications, t, showErrorModal]);
|
||||
}, [prepareEmailRequest, notifications, t, showErrorModal, selectedConfig.to]);
|
||||
|
||||
const prepareMsTeamsRequest = useCallback(
|
||||
() => ({
|
||||
@@ -329,6 +375,14 @@ function CreateAlertChannels({
|
||||
);
|
||||
|
||||
const onMsTeamsHandler = useCallback(async () => {
|
||||
if (!selectedConfig.webhook_url) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('webhook_url_required'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingState(true);
|
||||
|
||||
try {
|
||||
@@ -345,10 +399,24 @@ function CreateAlertChannels({
|
||||
} finally {
|
||||
setSavingState(false);
|
||||
}
|
||||
}, [prepareMsTeamsRequest, notifications, t, showErrorModal]);
|
||||
}, [
|
||||
selectedConfig.webhook_url,
|
||||
notifications,
|
||||
t,
|
||||
prepareMsTeamsRequest,
|
||||
showErrorModal,
|
||||
]);
|
||||
|
||||
const onSaveHandler = useCallback(
|
||||
async (value: ChannelType) => {
|
||||
if (!selectedConfig.name) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('channel_name_required'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const functionMapper = {
|
||||
[ChannelType.Slack]: onSlackHandler,
|
||||
[ChannelType.Webhook]: onWebhookHandler,
|
||||
@@ -477,26 +545,28 @@ function CreateAlertChannels({
|
||||
);
|
||||
|
||||
return (
|
||||
<FormAlertChannels
|
||||
{...{
|
||||
formInstance,
|
||||
onTypeChangeHandler,
|
||||
setSelectedConfig,
|
||||
type,
|
||||
onTestHandler,
|
||||
onSaveHandler,
|
||||
savingState,
|
||||
testingState,
|
||||
title: t('page_title_create'),
|
||||
initialValue: {
|
||||
<div className="create-alert-channels-container">
|
||||
<FormAlertChannels
|
||||
{...{
|
||||
formInstance,
|
||||
onTypeChangeHandler,
|
||||
setSelectedConfig,
|
||||
type,
|
||||
...selectedConfig,
|
||||
...PagerInitialConfig,
|
||||
...OpsgenieInitialConfig,
|
||||
...EmailInitialConfig,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
onTestHandler,
|
||||
onSaveHandler,
|
||||
savingState,
|
||||
testingState,
|
||||
title: t('page_title_create'),
|
||||
initialValue: {
|
||||
type,
|
||||
...selectedConfig,
|
||||
...PagerInitialConfig,
|
||||
...OpsgenieInitialConfig,
|
||||
...EmailInitialConfig,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function EditAlertChannels({
|
||||
@@ -53,7 +52,11 @@ function EditAlertChannels({
|
||||
const [savingState, setSavingState] = useState<boolean>(false);
|
||||
const [testingState, setTestingState] = useState<boolean>(false);
|
||||
const { notifications } = useNotifications();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
// Extract channelId from URL pathname since useParams doesn't work in nested routing
|
||||
const { pathname } = window.location;
|
||||
const channelIdMatch = pathname.match(/\/settings\/channels\/edit\/([^/]+)/);
|
||||
const id = channelIdMatch ? channelIdMatch[1] : '';
|
||||
|
||||
const [type, setType] = useState<ChannelType>(
|
||||
initialValue?.type ? (initialValue.type as ChannelType) : ChannelType.Slack,
|
||||
|
||||
@@ -1,30 +1,173 @@
|
||||
.empty-logs-search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 240px;
|
||||
|
||||
.empty-logs-search-container-content {
|
||||
.empty-logs-search {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 240px;
|
||||
}
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
align-items: flex-start;
|
||||
.empty-state-svg {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
&__sub-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-weight: 600;
|
||||
&__container {
|
||||
&--custom-message {
|
||||
height: 445px;
|
||||
.empty-state-svg {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
.empty-logs-search {
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 14px;
|
||||
color: var(--text-vanilla-400);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__description-list {
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
&__description-list li {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&__description-list li::before {
|
||||
content: '⎯';
|
||||
font-family: Inter;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--bg-robin-400);
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__clear-filters-btn {
|
||||
display: flex;
|
||||
width: 468px;
|
||||
font-family: Inter;
|
||||
padding: 12px;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-radius: 3px;
|
||||
border: 1px dashed var(--bg-slate-500);
|
||||
background: transparent;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
cursor: pointer;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&__clear-filters-btn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
max-width: 825px;
|
||||
gap: 25px;
|
||||
justify-content: center;
|
||||
margin-left: 21px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
&__resources-card {
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 4px;
|
||||
width: 332px;
|
||||
}
|
||||
|
||||
&__resources-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 16px 16px 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
&__resources-links {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.learn-more {
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,24 @@ import './EmptyLogsSearch.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import LearnMore from 'components/LearnMore/LearnMore';
|
||||
import { EmptyLogsListConfig } from 'container/LogsExplorerList/utils';
|
||||
import { Delete } from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
|
||||
|
||||
interface EmptyLogsSearchProps {
|
||||
dataSource: DataSource;
|
||||
panelType: PanelTypeKeys;
|
||||
customMessage?: EmptyLogsListConfig;
|
||||
}
|
||||
|
||||
export default function EmptyLogsSearch({
|
||||
dataSource,
|
||||
panelType,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
panelType: PanelTypeKeys;
|
||||
}): JSX.Element {
|
||||
customMessage,
|
||||
}: EmptyLogsSearchProps): JSX.Element {
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
@@ -30,18 +38,80 @@ export default function EmptyLogsSearch({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="empty-logs-search-container">
|
||||
<div className="empty-logs-search-container-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
<Typography.Text>
|
||||
<span className="sub-text">This query had no results. </span>
|
||||
Edit your query and try again!
|
||||
</Typography.Text>
|
||||
<div
|
||||
className={cx('empty-logs-search__container', {
|
||||
'empty-logs-search__container--custom-message': !!customMessage,
|
||||
})}
|
||||
>
|
||||
<div className="empty-logs-search__row">
|
||||
<div className="empty-logs-search__content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
{customMessage ? (
|
||||
<>
|
||||
<div className="empty-logs-search__header">
|
||||
<Typography.Text className="empty-logs-search__title">
|
||||
{customMessage.title}
|
||||
</Typography.Text>
|
||||
{customMessage.subTitle && (
|
||||
<Typography.Text className="empty-logs-search__subtitle">
|
||||
{customMessage.subTitle}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
{Array.isArray(customMessage.description) ? (
|
||||
<ul className="empty-logs-search__description-list">
|
||||
{customMessage.description.map((desc) => (
|
||||
<li key={desc}>{desc}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<Typography.Text className="empty-logs-search__description">
|
||||
{customMessage.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{/* Clear filters button */}
|
||||
{customMessage.showClearFiltersButton && (
|
||||
<button
|
||||
type="button"
|
||||
className="empty-logs-search__clear-filters-btn"
|
||||
onClick={customMessage.onClearFilters}
|
||||
>
|
||||
{customMessage.clearFiltersButtonText}
|
||||
<span className="empty-logs-search__clear-filters-btn-icon">
|
||||
<Delete size={14} />
|
||||
Clear filters
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text>
|
||||
<span className="empty-logs-search__sub-text">
|
||||
This query had no results.{' '}
|
||||
</span>
|
||||
Edit your query and try again!
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
{customMessage?.documentationLinks && (
|
||||
<div className="empty-logs-search__resources-card">
|
||||
<div className="empty-logs-search__resources-title">RESOURCES</div>
|
||||
<div className="empty-logs-search__resources-links">
|
||||
{customMessage.documentationLinks.map((link) => (
|
||||
<LearnMore key={link.text} text={link.text} url={link.url} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EmptyLogsSearch.defaultProps = {
|
||||
customMessage: null,
|
||||
};
|
||||
|
||||
@@ -24,6 +24,10 @@ import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
|
||||
import {
|
||||
MetricsExplorerEventKeys,
|
||||
MetricsExplorerEvents,
|
||||
} from 'container/MetricsExplorer/events';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
@@ -50,6 +54,7 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { FormattingOptions } from 'providers/preferences/types';
|
||||
import {
|
||||
CSSProperties,
|
||||
Dispatch,
|
||||
@@ -140,7 +145,9 @@ function ExplorerOptions({
|
||||
panelType,
|
||||
});
|
||||
} else if (isMetricsExplorer) {
|
||||
logEvent('Metrics Explorer: Save view clicked', {
|
||||
logEvent(MetricsExplorerEvents.SaveViewClicked, {
|
||||
[MetricsExplorerEventKeys.Tab]: 'explorer',
|
||||
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
@@ -184,8 +191,10 @@ function ExplorerOptions({
|
||||
panelType,
|
||||
});
|
||||
} else if (isMetricsExplorer) {
|
||||
logEvent('Metrics Explorer: Create alert', {
|
||||
logEvent(MetricsExplorerEvents.AddToAlertClicked, {
|
||||
panelType,
|
||||
[MetricsExplorerEventKeys.Tab]: 'explorer',
|
||||
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,11 +227,14 @@ function ExplorerOptions({
|
||||
panelType,
|
||||
});
|
||||
} else if (isMetricsExplorer) {
|
||||
logEvent('Metrics Explorer: Add to dashboard clicked', {
|
||||
logEvent(MetricsExplorerEvents.AddToDashboardClicked, {
|
||||
panelType,
|
||||
[MetricsExplorerEventKeys.Tab]: 'explorer',
|
||||
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
|
||||
});
|
||||
}
|
||||
setIsExport(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLogsExplorer, isMetricsExplorer, panelType, setIsExport, sourcepage]);
|
||||
|
||||
const {
|
||||
@@ -259,17 +271,26 @@ function ExplorerOptions({
|
||||
const getUpdatedExtraData = (
|
||||
extraData: string | undefined,
|
||||
newSelectedColumns: BaseAutocompleteData[],
|
||||
formattingOptions?: FormattingOptions,
|
||||
): string => {
|
||||
let updatedExtraData;
|
||||
|
||||
if (extraData) {
|
||||
const parsedExtraData = JSON.parse(extraData);
|
||||
parsedExtraData.selectColumns = newSelectedColumns;
|
||||
if (formattingOptions) {
|
||||
parsedExtraData.format = formattingOptions.format;
|
||||
parsedExtraData.maxLines = formattingOptions.maxLines;
|
||||
parsedExtraData.fontSize = formattingOptions.fontSize;
|
||||
}
|
||||
updatedExtraData = JSON.stringify(parsedExtraData);
|
||||
} else {
|
||||
updatedExtraData = JSON.stringify({
|
||||
color: Color.BG_SIENNA_500,
|
||||
selectColumns: newSelectedColumns,
|
||||
format: formattingOptions?.format,
|
||||
maxLines: formattingOptions?.maxLines,
|
||||
fontSize: formattingOptions?.fontSize,
|
||||
});
|
||||
}
|
||||
return updatedExtraData;
|
||||
@@ -278,6 +299,14 @@ function ExplorerOptions({
|
||||
const updatedExtraData = getUpdatedExtraData(
|
||||
extraData,
|
||||
options?.selectColumns,
|
||||
// pass this only for logs
|
||||
sourcepage === DataSource.LOGS
|
||||
? {
|
||||
format: options?.format,
|
||||
maxLines: options?.maxLines,
|
||||
fontSize: options?.fontSize,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -506,6 +535,14 @@ function ExplorerOptions({
|
||||
color,
|
||||
selectColumns: options.selectColumns,
|
||||
version: 1,
|
||||
...// pass this only for logs
|
||||
(sourcepage === DataSource.LOGS
|
||||
? {
|
||||
format: options?.format,
|
||||
maxLines: options?.maxLines,
|
||||
fontSize: options?.fontSize,
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
notifications,
|
||||
panelType: panelType || PANEL_TYPES.LIST,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user