Compare commits

...

105 Commits

Author SHA1 Message Date
Prashant Shahi
2c49ba444b Merge branch 'main' into chore/sample-flask 2024-09-02 08:45:26 +00:00
Prashant Shahi
262beef8f9 Merge pull request #5800 from SigNoz/release/v0.53.x
Release/v0.53.x
2024-08-30 15:20:27 +05:30
Prashant Shahi
43cc6dea92 chore(signoz): 📌 pin versions: SigNoz OtelCollector 0.102.7
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-08-30 15:06:52 +05:30
Prashant Shahi
6684640abe Merge branch 'develop' into release/v0.53.x 2024-08-30 12:50:35 +05:30
SagarRajput-7
363fb7bc34 feat: Kafka UI feedbacks (#5801)
* fix: solved kafka feature feedbacks

* fix: changed coming soon text to - join slack community
2024-08-30 12:00:52 +05:30
Srikanth Chekuri
dde4485839 chore: add types for alert type, state, and rule data kind (#5804) 2024-08-30 10:34:11 +05:30
Srikanth Chekuri
44598e304d chore: remove feature usage code from manager (#5803) 2024-08-29 21:53:28 +05:30
Srikanth Chekuri
4295a2756a chore: remove old data migrations (#5802) 2024-08-29 21:44:12 +05:30
Prashant Shahi
0a146910d6 chore(signoz): 📌 pin versions: SigNoz 0.53.0, SigNoz OtelCollector 0.102.6
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-08-29 19:25:34 +05:30
Prashant Shahi
690ed0f7f1 Merge branch 'main' into release/v0.53.x 2024-08-29 19:23:57 +05:30
SagarRajput-7
2f0d98ae51 chore: added trace views test (#5519)
* feat: added trace filter test cases

* feat: added trace filter test cases - initial render

* feat: added test cases - query sync, filter section behaviour etc

* feat: deleted mock-data files

* feat: added test cases of undefined filters and items

* feat: deleted tsconfig

* feat: added clear and rest btn test cases for traces filters

* feat: added collapse and uncollapse test for traces filters

* fix: added test cases for trace - saved view

* chore: code refactor'

* chore: added trace for search and navigation

* chore: used ROUTES enum

* chore: fixed test cases after merge conflict
2024-08-29 17:21:39 +05:30
SagarRajput-7
fb92ddc822 chore: added trace detail tests (#5523)
* feat: added trace filter test cases

* feat: added trace filter test cases - initial render

* feat: added test cases - query sync, filter section behaviour etc

* feat: deleted mock-data files

* feat: added test cases of undefined filters and items

* feat: deleted tsconfig

* feat: added clear and rest btn test cases for traces filters

* feat: added collapse and uncollapse test for traces filters

* chore: added trace detail tests

* chore: added trace detail tests - span selection, focus and reset

* chore: fixed eslint
2024-08-29 16:48:08 +05:30
Yunus M
15b0569b56 fix: show add credit card modal only for cloud users (#5797) 2024-08-29 16:47:27 +05:30
SagarRajput-7
140533b790 feat: added Messaging queue detail page (#5690)
* feat: added Messaging queue detail page

* feat: added MQDetails - tables - consumer, producer & network latency

* feat: added MQConfigOption - with dummy responses

* feat: configured query-range and autocomplete against the staging setup

* feat: added queryparams and linked config options with graph

* feat: added shareable link, cleanup code and connected details table with graph

* feat: fixed comments

* Messaging queue overview (#5782)

* feat: added messaging queue overview page

* feat: added get-started links

* feat: fixed comments

* feat: messaging queue misc tasks (#5785)

* feat: added lightMode styles

* feat: misc fix

* feat: misc fix

* feat: added customer tooltip info text

* feat: removed reset btn until the funcitonality is clear

* feat: fixed comments

* feat: fixed comments and added onDragSelect

* feat: added placeholder doc link for get-started for non-cloud
2024-08-29 16:36:56 +05:30
Prashant Shahi
710d22786d chore(sample-flask): reduce locust user and request rate by half
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-08-29 04:56:14 +00:00
Pranay Prateek
532f274bd6 fix: changing color of beta tag for Service Map & light theme changer (#5731)
* fix: changing color of tag for Service Map in sidepanel

* fix: added geekblue and borderless in beta tag to light theme changer

* fix: removed comments
2024-08-28 18:36:40 +05:30
rahulkeswani101
3200fd054e fix: redirect users to previous page after clicking back on onboarding flow instead of services page (#5685)
Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-08-28 17:48:02 +05:30
Srikanth Chekuri
8468cc863e fix: double encode composite query for explorer links (#5777) 2024-08-28 14:18:15 +05:30
Shivanshu Raj Shrivastava
71911687bf Merge pull request #5652 from shivanshuraj1333/feat/issues/1588
Add network latency (traces-metrics) correlation for kafka
2024-08-27 18:45:31 +05:30
Shivanshu Raj Shrivastava
9644297d28 Merge branch 'develop' into feat/issues/1588 2024-08-27 18:29:50 +05:30
shivanshu
faa6fdfcde feat: bug-fix in ClickHouseFormattedValue to allow strings 2024-08-27 18:28:52 +05:30
shivanshu
aabf364cc6 feat: add partition level granularity 2024-08-27 18:27:44 +05:30
Vikrant Gupta
4b861b2169 fix: remove the checks for aggregate operator in case of metrics v3/v4 (#5775) 2024-08-27 17:00:51 +05:30
shivanshu
8d655bf419 chore: use MinAllowedStepInterval 2024-08-27 14:04:00 +05:30
shivanshu
90cb8ba9a1 chore: modify producer output 2024-08-26 20:35:08 +05:30
shivanshu
f508ee7521 chore: query, response update 2024-08-26 20:28:22 +05:30
shivanshu
413caad0d8 chore: cleanup 2024-08-26 20:28:22 +05:30
shivanshu
666f601ecd feat: change to builder queries 2024-08-26 20:28:22 +05:30
shivanshu
5cdcbef00c feat: add network latency for kafka 2024-08-26 20:28:22 +05:30
Vikrant Gupta
c2f607ab6b chore: clean out the logs time range issues / old logs explorer routes issue (#5590)
* chore: clean out the logs time range async issues

* chore: correct the permissions for old logs explorer
2024-08-26 19:26:34 +05:30
Shivanshu Raj Shrivastava
2ca10bb87c Merge pull request #5769 from shivanshuraj1333/tmp
small patch to fix consumer_group check
2024-08-26 17:47:19 +05:30
Yunus M
6fb2a6d4c9 fix: copy to clipboard not copying complete value in case of numbers (#5770) 2024-08-26 16:48:07 +05:30
Shivanshu Raj Shrivastava
464589e0ca Merge branch 'develop' into tmp 2024-08-26 15:37:22 +05:30
shivanshu
3b94dab3ce chore: small patch to fix consumer_group check 2024-08-26 15:36:18 +05:30
Nityananda Gohain
9f481aacff feat: enable macro (#5760) 2024-08-26 11:57:18 +05:30
Prashant Shahi
9eb6c6201b Merge branch 'main' into chore/sample-flask 2024-08-26 05:57:28 +00:00
Vikrant Gupta
22f2e68db2 fix: colored logs in new logs explorer (#5749)
* fix: colored logs in new logs explorer

* fix: handle escapes better

* fix: handle escapes better

* chore: add code comments

* chore: added back text to copy to the body
2024-08-26 10:41:52 +05:30
Prashant Shahi
5bcf7de440 Merge pull request #5704 from SigNoz/release/v0.52.x
Release/v0.52.x
2024-08-16 21:00:32 +05:30
Prashant Shahi
703983a5f9 chore(signoz): 📌 pin versions: SigNoz 0.52.0, SigNoz OtelCollector 0.102.3
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-08-15 23:12:36 +05:30
Prashant Shahi
766a2123c5 Merge branch 'main' into release/v0.52.x 2024-08-15 13:42:02 +05:30
Prashant Shahi
c13b347808 Merge branch 'main' into chore/sample-flask 2024-08-01 08:01:04 +00:00
Prashant Shahi
a476c68f7e Merge pull request #5618 from SigNoz/release/v0.51.x
Release/v0.51.x
2024-07-31 22:30:09 +05:30
Prashant Shahi
fc15aa6f1c Merge branch 'develop' into release/v0.51.x 2024-07-31 21:29:07 +05:30
Prashant Shahi
4192fd573d chore(signoz): 📌 pin versions: SigNoz 0.51.0, SigNoz OtelCollector 0.102.3
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-07-31 20:29:09 +05:30
Prashant Shahi
ca13d80205 Merge branch 'main' into release/v0.51.x 2024-07-31 20:27:51 +05:30
Prashant Shahi
610edbb3d1 Merge branch 'main' into chore/sample-flask 2024-07-17 14:55:28 +00:00
Prashant Shahi
8d84ce8f06 Merge pull request #5509 from SigNoz/release/v0.50.x
Release/v0.50.x
2024-07-17 20:16:19 +05:30
Prashant Shahi
09ea7b9eb5 chore(signoz): 📌 pin versions: SigNoz 0.50.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-07-17 19:01:03 +05:30
Prashant Shahi
04991ca4a2 Merge branch 'main' into chore/sample-flask 2024-07-09 18:40:55 +00:00
Prashant Shahi
9c7e1a0581 Merge branch 'main' into chore/sample-flask 2024-07-04 14:56:45 +00:00
Prashant Shahi
e3484dcfa9 Merge branch 'main' into chore/sample-flask 2024-06-27 18:41:01 +00:00
Prashant Shahi
e6de113ff0 Merge branch 'main' into chore/sample-flask 2024-06-21 04:42:50 +00:00
Prashant Shahi
c8045243b5 Merge branch 'main' into chore/sample-flask 2024-06-05 14:24:43 +00:00
Prashant Shahi
4fd862e7ff Merge branch 'main' into chore/sample-flask 2024-05-23 15:34:22 +00:00
Prashant Shahi
950eb99de0 Merge branch 'main' into chore/sample-flask 2024-05-20 03:59:18 +00:00
Prashant Shahi
078aaafc57 Merge branch 'main' into chore/sample-flask 2024-04-15 15:44:24 +00:00
Prashant Shahi
04564f63a8 Merge branch 'main' into chore/sample-flask 2024-03-27 18:42:25 +00:00
Prashant Shahi
862b8b92bd Merge branch 'main' into chore/sample-flask 2024-02-28 15:37:03 +00:00
Prashant Shahi
7b0f5976b4 Merge branch 'main' into chore/sample-flask 2024-02-21 14:25:08 +00:00
Prashant Shahi
b5d01705df Merge branch 'main' into chore/sample-flask 2024-02-14 18:10:09 +00:00
Prashant Shahi
8f1fecc5ba Merge branch 'main' into chore/sample-flask 2024-01-31 17:18:27 +00:00
Prashant Shahi
902eead23c Merge branch 'main' into chore/sample-flask 2024-01-19 22:38:36 +00:00
Prashant Shahi
ffa414fc00 Merge branch 'main' into chore/sample-flask 2023-12-22 13:15:46 +00:00
Prashant Shahi
aa3f1bae62 Merge branch 'main' into chore/sample-flask 2023-12-14 07:25:09 +00:00
Prashant Shahi
16ef43b6ea Merge branch 'main' into chore/sample-flask 2023-12-07 07:35:14 +00:00
Prashant Shahi
e24c1bd400 Merge branch 'main' into chore/sample-flask 2023-11-30 04:32:50 +00:00
Prashant Shahi
6a3054d871 Merge branch 'main' into chore/sample-flask 2023-11-23 17:31:47 +00:00
Prashant Shahi
6bebed62b7 Merge branch 'main' into chore/sample-flask 2023-11-22 17:17:38 +00:00
Prashant Shahi
a7a114cac0 Merge branch 'main' into chore/sample-flask 2023-11-21 13:04:40 +00:00
Prashant Shahi
7ce5fae681 Merge branch 'main' into chore/sample-flask 2023-11-16 20:18:38 +00:00
Prashant Shahi
d1aaa49815 Merge branch 'main' into chore/sample-flask 2023-11-02 19:07:59 +00:00
Prashant Shahi
7a6e0bf87f Merge branch 'main' into chore/sample-flask 2023-11-01 18:24:46 +00:00
Prashant Shahi
a5cfb3abc7 Merge branch 'main' into chore/sample-flask 2023-10-25 18:48:32 +00:00
Prashant Shahi
f732328cf5 Merge branch 'main' into chore/sample-flask 2023-10-23 17:22:34 +00:00
Prashant Shahi
9fa37afab2 Merge branch 'main' into chore/sample-flask 2023-10-13 02:36:12 +00:00
Prashant Shahi
cc35f7db3e Merge branch 'main' into chore/sample-flask 2023-09-27 20:37:56 +00:00
Prashant Shahi
861b0646d6 Merge branch 'main' into chore/sample-flask 2023-09-21 06:35:25 +00:00
Prashant Shahi
cd4682e67e Merge branch 'main' into chore/sample-flask 2023-09-19 17:41:24 +00:00
Prashant Shahi
32baa11beb Merge branch 'main' into chore/sample-flask 2023-09-18 14:48:17 +00:00
Prashant Shahi
8ae04f79fd Merge branch 'main' into chore/sample-flask 2023-09-14 14:20:25 +00:00
Prashant Shahi
ddd1c8e9ff Merge branch 'main' into chore/sample-flask 2023-09-04 12:30:15 +00:00
Prashant Shahi
cb063893b2 Merge branch 'main' into chore/sample-flask 2023-08-24 11:39:32 +00:00
Prashant Shahi
8898f6707f chore: update install.sh script 2023-08-18 13:29:03 +00:00
Prashant Shahi
bd8f16c5fd Merge branch 'main' into chore/sample-flask 2023-08-18 13:28:01 +00:00
Prashant Shahi
1431df9c9d Merge branch 'main' into chore/sample-flask 2023-08-18 03:50:33 +00:00
Prashant Shahi
5df1234892 Merge branch 'main' into chore/sample-flask 2023-08-11 10:30:29 +00:00
Prashant Shahi
0d24f5b428 Merge branch 'main' into chore/sample-flask 2023-08-11 08:22:11 +00:00
Prashant Shahi
55b70ae215 Merge branch 'main' into chore/sample-flask 2023-08-06 16:32:47 +00:00
Prashant Shahi
1ca3957ecb Merge branch 'main' into chore/sample-flask 2023-08-04 13:29:12 +00:00
Prashant Shahi
6be97bcb60 Merge branch 'main' into chore/sample-flask 2023-08-02 19:43:54 +00:00
Prashant Shahi
ac6ea3872c Merge branch 'main' into chore/sample-flask 2023-07-31 02:18:50 +00:00
Prashant Shahi
e33e3f89ac Merge branch 'main' into chore/sample-flask 2023-07-20 11:32:18 +00:00
Prashant Shahi
4bc4e0ed12 Merge branch 'main' into chore/sample-flask 2023-07-18 09:02:02 +00:00
Prashant Shahi
5b29c5989e Merge branch 'main' into chore/sample-flask 2023-07-15 11:14:52 +00:00
Prashant Shahi
2477612eef Merge branch 'main' into chore/sample-flask 2023-07-05 20:12:13 +00:00
Prashant Shahi
5df2c491cf Merge branch 'main' into chore/sample-flask 2023-06-24 06:50:56 +00:00
Prashant Shahi
0639ea8d17 Merge branch 'main' into chore/sample-flask 2023-06-09 12:13:01 +00:00
Prashant Shahi
19ebf81524 Merge branch 'main' into chore/sample-flask 2023-06-08 14:58:26 +00:00
Prashant Shahi
278a19a558 Merge branch 'main' into chore/sample-flask 2023-05-21 18:32:47 +00:00
Prashant Shahi
0b9ee9c3fd Merge branch 'main' into chore/sample-flask 2023-05-04 08:04:35 +00:00
Prashant Shahi
2da99428ae chore: 🔧 reduce locust interval
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-04-23 15:09:30 +00:00
Hello SigNoz
e5b02cb8d9 Merge branch 'main' into chore/sample-flask 2023-04-22 12:50:58 +00:00
Prashant Shahi
88c02e713d chore: 🙈 update gitignore file to ignore all pycache
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-04-05 11:17:25 +05:45
Prashant Shahi
8a395ce8e5 chore: 🔧 update locustfile to perform CRUD
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-04-05 11:16:12 +05:45
Prashant Shahi
8fb3e0ed54 chore: 🔧 sample-flask docker-compose YAML with exception generator
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-04-05 01:26:58 +05:45
101 changed files with 5278 additions and 660 deletions

2
.gitignore vendored
View File

@@ -34,7 +34,7 @@ frontend/src/constants/env.ts
**/.vscode
**/build
**/storage
**/locust-scripts/__pycache__/
**/__pycache__/
**/__debug_bin
.env

View File

@@ -649,12 +649,12 @@
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
-->
<!--
<macros>
<shard>01</shard>
<replica>example01-01-1</replica>
</macros>
-->
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->

View File

@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.49.1
image: signoz/query-service:0.53.0
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.48.0
image: signoz/frontend:0.53.0
deploy:
restart_policy:
condition: on-failure
@@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.102.2
image: signoz/signoz-otel-collector:0.102.7
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -238,7 +238,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.102.2
image: signoz/signoz-schema-migrator:0.102.7
deploy:
restart_policy:
condition: on-failure

View File

@@ -649,12 +649,12 @@
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
-->
<!--
<macros>
<shard>01</shard>
<replica>example01-01-1</replica>
</macros>
-->
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->

View File

@@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -81,7 +81,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.102.2
image: signoz/signoz-otel-collector:0.102.7
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -0,0 +1,42 @@
version: "2.4"
services:
mongodb:
image: "mongo:latest"
container_name: mongodb
hostname: mongodb
restart: always
# environment:
# MONGO_INITDB_ROOT_USERNAME: root
# MONGO_INITDB_ROOT_PASSWORD: example
# ports:
# - 27017:27017
sample-flask:
image: "signoz/sample-flask-app:latest"
container_name: sample-flask
hostname: sample-flask
restart: always
ports:
- 5002:5002
extra_hosts:
- signoz:host-gateway
environment:
MONGO_HOST: mongodb
OTEL_RESOURCE_ATTRIBUTES: service.name=sample-flask
OTEL_EXPORTER_OTLP_ENDPOINT: http://signoz:4317
load-flask:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
container_name: load-flask
hostname: load-flask
restart: always
environment:
ATTACKED_HOST: http://sample-flask:5002
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 45
TASK_DELAY_TO: 60
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 5 -r 5"
volumes:
- ../common/locust-flask:/locust

View File

@@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
image: signoz/query-service:${DOCKER_TAG:-0.53.0}
container_name: signoz-query-service
command:
[
@@ -204,7 +204,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
image: signoz/frontend:${DOCKER_TAG:-0.53.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -216,7 +216,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -230,7 +230,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.7}
container_name: signoz-otel-collector
command:
[

View File

@@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
image: signoz/query-service:${DOCKER_TAG:-0.53.0}
container_name: signoz-query-service
command:
[
@@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
image: signoz/frontend:${DOCKER_TAG:-0.53.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -229,7 +229,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.7}
container_name: signoz-otel-collector
command:
[

View File

@@ -0,0 +1,20 @@
from locust import HttpUser, task, between
from uuid import uuid4
class UserTasks(HttpUser):
wait_time = between(30, 60)
@task(1)
def list(self):
self.client.get("/list")
@task(1)
def add_todo(self):
self.client.post("/action", data={"name": "new-todo-"+str(uuid4()), "desc":"new desc", "date": "1990-04-10", "pr":"1"})
@task(1)
def update(self):
self.client.post("/action3", data={"_id":"626682d44bd2839cd80eb079", "name":"todo-"+str(uuid4()), "desc": "update desc", "date": "1990-04-11", "pr":"2"})
@task(1)
def generate_error(self):
self.client.get("/generate-error")

View File

@@ -494,7 +494,7 @@ fi
start_docker
# $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up -d --remove-orphans || true
# $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml -f ./docker/clickhouse-setup/docker-compose-flask.yaml up -d --remove-orphans || true
echo ""
@@ -506,7 +506,7 @@ echo "🟡 Starting the SigNoz containers. It may take a few minutes ..."
echo
# The docker-compose command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the
# script doesn't exit because this command looks like it failed to do it's thing.
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up --detach --remove-orphans || true
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml -f ./docker/clickhouse-setup/docker-compose-flask.yaml up --detach --remove-orphans || true
wait_for_containers_start 60
echo ""

View File

@@ -49,5 +49,6 @@
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS": "SigNoz | Integrations"
"INTEGRATIONS": "SigNoz | Integrations",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
}

View File

@@ -204,3 +204,15 @@ export const InstalledIntegrations = Loadable(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
),
);
export const MessagingQueues = Loadable(
() =>
import(/* webpackChunkName: "MessagingQueues" */ 'pages/MessagingQueues'),
);
export const MQDetailPage = Loadable(
() =>
import(
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
),
);

View File

@@ -23,6 +23,8 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueues,
MQDetailPage,
MySettings,
NewDashboardPage,
OldLogsExplorer,
@@ -351,6 +353,20 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'INTEGRATIONS',
},
{
path: ROUTES.MESSAGING_QUEUES,
exact: true,
component: MessagingQueues,
key: 'MESSAGING_QUEUES',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_DETAIL,
exact: true,
component: MQDetailPage,
key: 'MESSAGING_QUEUES_DETAIL',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -2,6 +2,7 @@
import './LogDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import Convert from 'ansi-to-html';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
@@ -10,8 +11,13 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import {
aggregateAttributesResourcesToString,
removeEscapeCharacters,
unescapeString,
} from 'container/LogDetailedView/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import dompurify from 'dompurify';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
@@ -28,11 +34,14 @@ import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
import { VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces';
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
const convert = new Convert();
function LogDetail({
log,
onClose,
@@ -90,6 +99,17 @@ function LogDetail({
}
};
const htmlBody = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(unescapeString(log?.body || ''), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),
}),
[log?.body],
);
const handleJSONCopy = (): void => {
copyToClipboard(LogJsonData);
notifications.success({
@@ -127,8 +147,8 @@ function LogDetail({
>
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={log?.body} placement="left">
<Typography.Text className="log-body">{log?.body}</Typography.Text>
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
<div className="log-overflow-shadow">&nbsp;</div>

View File

@@ -3,14 +3,14 @@
display: flex;
align-items: center;
&.small {
height: 16px;
line-height: 16px;
}
&.medium {
height: 20px;
line-height: 20px;
}
&.large {
height: 24px;
line-height: 24px;
}
}

View File

@@ -4,6 +4,7 @@ import { ReactNode, useCallback, useEffect } from 'react';
import { useCopyToClipboard } from 'react-use';
function CopyClipboardHOC({
entityKey,
textToCopy,
children,
}: CopyClipboardHOCProps): JSX.Element {
@@ -11,11 +12,15 @@ function CopyClipboardHOC({
const { notifications } = useNotifications();
useEffect(() => {
if (value.value) {
const key = entityKey || '';
const notificationMessage = `${key} copied to clipboard`;
notifications.success({
message: 'Copied to clipboard',
message: notificationMessage,
});
}
}, [value, notifications]);
}, [value, notifications, entityKey]);
const onClick = useCallback((): void => {
setCopy(textToCopy);
@@ -34,6 +39,7 @@ function CopyClipboardHOC({
}
interface CopyClipboardHOCProps {
entityKey: string | undefined;
textToCopy: string;
children: ReactNode;
}

View File

@@ -6,6 +6,7 @@ import { Typography } from 'antd';
import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
@@ -56,7 +57,7 @@ function LogGeneralField({
const html = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(fieldValue, {
dompurify.sanitize(unescapeString(fieldValue), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),

View File

@@ -4,6 +4,7 @@ import Convert from 'ansi-to-html';
import { DrawerProps } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
@@ -145,7 +146,9 @@ function RawLogView({
const html = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(text, { FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS] }),
dompurify.sanitize(unescapeString(text), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),
}),
[text],

View File

@@ -4,6 +4,7 @@ import Convert from 'ansi-to-html';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import cx from 'classnames';
import { unescapeString } from 'container/LogDetailedView/utils';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -115,7 +116,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
<TableBodyContent
dangerouslySetInnerHTML={{
__html: convert.toHtml(
dompurify.sanitize(field, {
dompurify.sanitize(unescapeString(field), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),

View File

@@ -32,4 +32,8 @@ export enum QueryParams {
relativeTime = 'relativeTime',
alertType = 'alertType',
ruleId = 'ruleId',
consumerGrp = 'consumerGrp',
topic = 'topic',
partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery',
}

View File

@@ -8,4 +8,5 @@ export const REACT_QUERY_KEY = {
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
};

View File

@@ -54,6 +54,8 @@ const ROUTES = {
WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts',
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
} as const;
export default ROUTES;

View File

@@ -9,6 +9,7 @@ export const GlobalShortcuts = {
NavigateToDashboards: 'd+shift',
NavigateToAlerts: 'a+shift',
NavigateToExceptions: 'e+shift',
NavigateToMessagingQueues: 'm+shift',
};
export const GlobalShortcutsName = {
@@ -19,6 +20,7 @@ export const GlobalShortcutsName = {
NavigateToDashboards: 'shift+d',
NavigateToAlerts: 'shift+a',
NavigateToExceptions: 'shift+e',
NavigateToMessagingQueues: 'shift+m',
};
export const GlobalShortcutsDescription = {
@@ -29,4 +31,5 @@ export const GlobalShortcutsDescription = {
NavigateToDashboards: 'Navigate to dashboards page',
NavigateToAlerts: 'Navigate to alerts page',
NavigateToExceptions: 'Navigate to Exceptions page',
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
};

View File

@@ -47,6 +47,7 @@ import {
UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { ChildrenContainer, Layout, LayoutContent } from './styles';
@@ -71,7 +72,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const isChatSupportEnabled =
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
const isCloudUserVal = isCloudUser();
const showAddCreditCardModal =
isChatSupportEnabled &&
isCloudUserVal &&
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
@@ -241,6 +249,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
const isMessagingQueues = (): boolean =>
routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL';
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isDashboardView = (): boolean => {
/**
@@ -329,7 +340,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView()
isDashboardListView() ||
isMessagingQueues()
? 0
: '0 1rem',
}}

View File

@@ -47,6 +47,7 @@ function WidgetGraphComponent({
setRequestData,
onClickHandler,
onDragSelect,
customTooltipElement,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
@@ -335,6 +336,7 @@ function WidgetGraphComponent({
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
/>
</div>
)}

View File

@@ -33,6 +33,7 @@ function GridCardGraph({
version,
onClickHandler,
onDragSelect,
customTooltipElement,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@@ -215,6 +216,7 @@ function GridCardGraph({
setRequestData={setRequestData}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
customTooltipElement={customTooltipElement}
/>
)}
</div>

View File

@@ -31,6 +31,7 @@ export interface WidgetGraphComponentProps {
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
onClickHandler?: OnClickPluginOpts['onClick'];
onDragSelect: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
}
export interface GridCardGraphProps {
@@ -42,6 +43,7 @@ export interface GridCardGraphProps {
variables?: Dashboard['data']['variables'];
version?: string;
onDragSelect: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -18,6 +18,7 @@
.tags {
display: flex;
gap: 8;
flex-wrap: wrap;
gap: 8px;
}
}

View File

@@ -22,6 +22,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem';
import TableView from './TableView';
import { removeEscapeCharacters } from './utils';
interface OverviewProps {
logData: ILog;
@@ -124,7 +125,7 @@ function Overview({
children: (
<div className="logs-body-content">
<MEditor
value={logData.body}
value={removeEscapeCharacters(logData.body)}
language="json"
options={options}
onChange={(): void => {}}

View File

@@ -1,17 +1,20 @@
import './TableViewActions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import Convert from 'ansi-to-html';
import { Button, Popover, Spin, Tooltip, Tree } from 'antd';
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import dompurify from 'dompurify';
import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
import { DataType } from '../TableView';
import {
@@ -19,6 +22,7 @@ import {
jsonToDataNodes,
recursiveParseJSON,
removeEscapeCharacters,
unescapeString,
} from '../utils';
interface ITableViewActionsProps {
@@ -39,6 +43,8 @@ interface ITableViewActionsProps {
) => () => void;
}
const convert = new Convert();
export function TableViewActions(
props: ITableViewActionsProps,
): React.ReactElement {
@@ -61,7 +67,7 @@ export function TableViewActions(
);
const [isOpen, setIsOpen] = useState<boolean>(false);
const textToCopy = fieldData.value.slice(1, -1);
const textToCopy = fieldData.value;
if (record.field === 'body') {
const parsedBody = recursiveParseJSON(fieldData.value);
@@ -71,22 +77,45 @@ export function TableViewActions(
);
}
}
const bodyHtml =
record.field === 'body'
? {
__html: convert.toHtml(
dompurify.sanitize(unescapeString(record.value), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),
}
: { __html: '' };
const fieldFilterKey = filterKeyForField(fieldData.field);
return (
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
<CopyClipboardHOC textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>
{record.field === 'body' ? (
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
dangerouslySetInnerHTML={bodyHtml}
/>
</CopyClipboardHOC>
) : (
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>
)}
{!isListViewPanel && (
<span className="action-btn">

View File

@@ -250,19 +250,37 @@ export const getDataTypes = (value: unknown): DataTypes => {
return determineType(value);
};
// now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make
// the log line readable
export const removeEscapeCharacters = (str: string): string =>
str.replace(/\\([ntfr'"\\])/g, (_: string, char: string) => {
const escapeMap: Record<string, string> = {
n: '\n',
t: '\t',
f: '\f',
r: '\r',
"'": "'",
'"': '"',
'\\': '\\',
};
return escapeMap[char as keyof typeof escapeMap];
});
str
.replace(/\\x1[bB][[0-9;]*m/g, '')
.replace(/\\u001[bB][[0-9;]*m/g, '')
.replace(/\\x[0-9A-Fa-f]{2}/g, '')
.replace(/\\u[0-9A-Fa-f]{4}/g, '')
.replace(/\\[btnfrv0'"\\]/g, '');
// we need to remove the escape from the escaped characters as some recievers like file log escape the unicode escape characters.
// example: Log [\u001B[32;1mThis is bright green\u001B[0m] is being sent as [\\u001B[32;1mThis is bright green\\u001B[0m]
//
// so we need to remove this escapes to render the color properly
export const unescapeString = (str: string): string =>
str
.replace(/\\n/g, '\n') // Replaces escaped newlines
.replace(/\\r/g, '\r') // Replaces escaped carriage returns
.replace(/\\t/g, '\t') // Replaces escaped tabs
.replace(/\\b/g, '\b') // Replaces escaped backspaces
.replace(/\\f/g, '\f') // Replaces escaped form feeds
.replace(/\\v/g, '\v') // Replaces escaped vertical tabs
.replace(/\\'/g, "'") // Replaces escaped single quotes
.replace(/\\"/g, '"') // Replaces escaped double quotes
.replace(/\\\\/g, '\\') // Replaces escaped backslashes
.replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) =>
String.fromCharCode(parseInt(hex, 16)),
) // Replaces hexadecimal escape sequences
.replace(/\\u([0-9A-Fa-f]{4})/g, (_, hex) =>
String.fromCharCode(parseInt(hex, 16)),
); // Replaces Unicode escape sequences
export function removeExtraSpaces(input: string): string {
return input.replace(/\s+/g, ' ').trim();

View File

@@ -155,6 +155,7 @@ function LogsExplorerList({
>
<OverlayScrollbar isVirtuoso>
<Virtuoso
key={activeLogIndex || 'logs-virtuoso'}
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}

View File

@@ -100,14 +100,14 @@ function LogsExplorerViews({
// this is to respect the panel type present in the URL rather than defaulting it to list always.
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
const { activeLogId, onTimeRangeChange } = useCopyLogLink();
const { queryData: pageSize } = useUrlQueryData(
QueryParams.pageSize,
DEFAULT_PER_PAGE_VALUE,
);
const { minTime } = useSelector<AppState, GlobalReducer>(
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@@ -254,11 +254,10 @@ function LogsExplorerViews({
enabled: !isLimit && !!requestData,
},
{
...(timeRange &&
activeLogId &&
...(activeLogId &&
!logs.length && {
start: timeRange.start,
end: timeRange.end,
start: minTime,
end: maxTime,
}),
},
undefined,
@@ -521,7 +520,7 @@ function LogsExplorerViews({
setLogs(newLogs);
onTimeRangeChange({
start: currentParams?.start,
end: timeRange?.end || currentParams?.end,
end: currentParams?.end,
pageSize: newLogs.length,
});
}
@@ -538,8 +537,7 @@ function LogsExplorerViews({
filters: listQuery?.filters || initialFilters,
page: 1,
log: null,
pageSize:
timeRange?.pageSize && activeLogId ? timeRange?.pageSize : pageSize,
pageSize,
});
setLogs([]);
@@ -554,7 +552,6 @@ function LogsExplorerViews({
listQuery,
pageSize,
minTime,
timeRange,
activeLogId,
onTimeRangeChange,
panelType,

View File

@@ -10,12 +10,14 @@ import LogsTableView from 'components/Logs/TableView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { FontSize } from 'container/OptionsMenu/types';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso';
import { AppState } from 'store/reducers';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
// interfaces
import { ILogsReducer } from 'types/reducer/logs';
@@ -57,6 +59,14 @@ function LogsTable(props: LogsTableProps): JSX.Element {
liveTail,
]);
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
// this component will alwyays be called on old logs explorer page itself!
dataSource: DataSource.LOGS,
// and we do not have table / timeseries aggregated views in the old logs explorer!
aggregateOperator: StringOperators.NOOP,
});
const getItemContent = useCallback(
(index: number): JSX.Element => {
const log = logs[index];
@@ -68,7 +78,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
data={log}
linesPerRow={linesPerRow}
selectedFields={selected}
fontSize={FontSize.SMALL}
fontSize={options.fontSize}
/>
);
}
@@ -81,11 +91,19 @@ function LogsTable(props: LogsTableProps): JSX.Element {
linesPerRow={linesPerRow}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={FontSize.SMALL}
fontSize={options.fontSize}
/>
);
},
[logs, viewMode, selected, onAddToQuery, onSetActiveLog, linesPerRow],
[
logs,
viewMode,
selected,
linesPerRow,
onAddToQuery,
onSetActiveLog,
options.fontSize,
],
);
const renderContent = useMemo(() => {
@@ -96,7 +114,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
logs={logs}
fields={selected}
linesPerRow={linesPerRow}
fontSize={FontSize.SMALL}
fontSize={options.fontSize}
/>
);
}
@@ -108,7 +126,15 @@ function LogsTable(props: LogsTableProps): JSX.Element {
</OverlayScrollbar>
</Card>
);
}, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]);
}, [
getItemContent,
linesPerRow,
logs,
onSetActiveLog,
options.fontSize,
selected,
viewMode,
]);
if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />;

View File

@@ -26,7 +26,9 @@ function MySettings(): JSX.Element {
label: (
<div className="theme-option">
<Sun size={12} data-testid="light-theme-icon" /> Light{' '}
<Tag color="magenta">Beta</Tag>
<Tag bordered={false} color="geekblue">
Beta
</Tag>
</div>
),
value: 'light',

View File

@@ -247,8 +247,7 @@ export default function Onboarding(): JSX.Element {
const handleNext = (): void => {
if (activeStep <= 3) {
handleNextStep();
history.replace(moduleRouteMap[selectedModule.id as ModulesMap]);
history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
}
};
@@ -258,6 +257,13 @@ export default function Onboarding(): JSX.Element {
updateSelectedDataSource(null);
};
const handleBackNavigation = (): void => {
setCurrent(0);
setActiveStep(1);
setSelectedModule(useCases.APM);
resetProgress();
};
useEffect(() => {
const { pathname } = location;
@@ -277,9 +283,11 @@ export default function Onboarding(): JSX.Element {
} else if (pathname === ROUTES.GET_STARTED_AZURE_MONITORING) {
handleModuleSelect(useCases.AzureMonitoring);
handleNextStep();
} else {
handleBackNavigation();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [location.pathname]);
const [form] = Form.useForm<InviteMemberFormValues>();
const [

View File

@@ -15,6 +15,7 @@ function PanelWrapper({
onDragSelect,
selectedGraph,
tableProcessedDataRef,
customTooltipElement,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
selectedGraph || widget.panelTypes
@@ -37,6 +38,7 @@ function PanelWrapper({
onDragSelect={onDragSelect}
selectedGraph={selectedGraph}
tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
/>
);
}

View File

@@ -30,6 +30,7 @@ function UplotPanelWrapper({
onClickHandler,
onDragSelect,
selectedGraph,
customTooltipElement,
}: PanelWrapperProps): JSX.Element {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
@@ -126,6 +127,7 @@ function UplotPanelWrapper({
stackBarChart: widget?.stackedBarChart,
hiddenGraph,
setHiddenGraph,
customTooltipElement,
}),
[
widget?.id,
@@ -147,6 +149,7 @@ function UplotPanelWrapper({
selectedGraph,
currentQuery,
hiddenGraph,
customTooltipElement,
],
);

View File

@@ -23,6 +23,7 @@ export type PanelWrapperProps = {
onDragSelect: (start: number, end: number) => void;
selectedGraph?: PANEL_TYPES;
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
customTooltipElement?: HTMLDivElement;
};
export type TooltipData = {

View File

@@ -62,7 +62,9 @@ export const AggregatorFilter = memo(function AggregatorFilter({
dataSource: query.dataSource,
}),
{
enabled: !!query.aggregateOperator && !!query.dataSource,
enabled:
query.dataSource === DataSource.METRICS ||
(!!query.aggregateOperator && !!query.dataSource),
onSuccess: (data) => {
const options: ExtendedSelectOption[] =
data?.payload?.attributeKeys?.map(({ id: _, ...item }) => ({

View File

@@ -75,6 +75,10 @@
text-overflow: ellipsis;
}
}
.beta-tag {
padding-right: 0;
}
}
.lightMode {

View File

@@ -24,14 +24,16 @@ export default function NavItem({
onClick={(event): void => onClick(event)}
>
<div className="nav-item-active-marker" />
<div className="nav-item-data">
<div className={cx('nav-item-data', isBeta ? 'beta-tag' : '')}>
<div className="nav-item-icon">{icon}</div>
<div className="nav-item-label">{label}</div>
{isBeta && (
<div className="nav-item-beta">
<Tag color="magenta">Beta</Tag>
<Tag bordered={false} color="geekblue">
Beta
</Tag>
</div>
)}
</div>

View File

@@ -347,6 +347,10 @@ function SideNav({
onClickHandler(ROUTES.ALL_DASHBOARD, null),
);
registerShortcut(GlobalShortcuts.NavigateToMessagingQueues, () =>
onClickHandler(ROUTES.MESSAGING_QUEUES, null),
);
registerShortcut(GlobalShortcuts.NavigateToAlerts, () =>
onClickHandler(ROUTES.LIST_ALL_ALERT, null),
);
@@ -362,6 +366,7 @@ function SideNav({
deregisterShortcut(GlobalShortcuts.NavigateToDashboards);
deregisterShortcut(GlobalShortcuts.NavigateToAlerts);
deregisterShortcut(GlobalShortcuts.NavigateToExceptions);
deregisterShortcut(GlobalShortcuts.NavigateToMessagingQueues);
};
}, [deregisterShortcut, onClickHandler, onCollapse, registerShortcut]);

View File

@@ -48,4 +48,6 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.TRACE_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.LOGS_PIPELINES]: [QueryParams.resourceAttributes],
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
[ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes],
[ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes],
};

View File

@@ -10,6 +10,7 @@ import {
FileKey2,
Layers2,
LayoutGrid,
ListMinus,
MessageSquare,
Receipt,
Route,
@@ -86,6 +87,12 @@ const menuItems: SidebarItem[] = [
label: 'Dashboards',
icon: <LayoutGrid size={16} />,
},
{
key: ROUTES.MESSAGING_QUEUES,
label: 'Messaging Queues',
icon: <ListMinus size={16} />,
isBeta: true,
},
{
key: ROUTES.LIST_ALL_ALERT,
label: 'Alerts',

View File

@@ -27,6 +27,7 @@ const breadcrumbNameMap: Record<string, string> = {
[ROUTES.BILLING]: 'Billing',
[ROUTES.SUPPORT]: 'Support',
[ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked',
[ROUTES.MESSAGING_QUEUES]: 'Messaging Queues',
};
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@@ -208,6 +208,8 @@ export const routesToSkip = [
ROUTES.DASHBOARD,
ROUTES.DASHBOARD_WIDGET,
ROUTES.SERVICE_TOP_LEVEL_OPERATIONS,
ROUTES.MESSAGING_QUEUES,
ROUTES.MESSAGING_QUEUES_DETAIL,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@@ -468,7 +468,6 @@ function DateTimeSelection({
if (updatedTime !== 'custom') {
urlQuery.delete('startTime');
urlQuery.delete('endTime');
urlQuery.set(QueryParams.relativeTime, updatedTime);
} else {
const startTime = preStartTime.toString();
@@ -476,6 +475,7 @@ function DateTimeSelection({
urlQuery.set(QueryParams.startTime, startTime);
urlQuery.set(QueryParams.endTime, endTime);
urlQuery.delete(QueryParams.relativeTime);
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;

View File

@@ -219,10 +219,18 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
<Col flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`} />
<Col flex="auto">
<StyledSpace styledclass={[styles.floatRight]}>
<Button onClick={onFocusSelectedSpanHandler} icon={<FilterOutlined />}>
<Button
onClick={onFocusSelectedSpanHandler}
icon={<FilterOutlined />}
data-testid="span-focus-btn"
>
Focus on selected span
</Button>
<Button type="default" onClick={onResetHandler}>
<Button
type="default"
onClick={onResetHandler}
data-testid="reset-focus"
>
Reset Focus
</Button>
</StyledSpace>
@@ -262,6 +270,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
collapsedWidth={40}
defaultCollapsed
onCollapse={(value): void => setCollapsed(value)}
data-testid="span-details-sider"
>
{!collapsed && (
<StyledCol styledclass={[styles.selectedSpanDetailContainer]}>

View File

@@ -12,7 +12,6 @@ export type UseCopyLogLink = {
isHighlighted: boolean;
isLogsExplorerPage: boolean;
activeLogId: string | null;
timeRange: LogTimeRange | null;
onLogCopy: MouseEventHandler<HTMLElement>;
onTimeRangeChange: (newTimeRange: LogTimeRange | null) => void;
};

View File

@@ -26,33 +26,25 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
const { queryData: timeRange } = useUrlQueryData<LogTimeRange | null>(
QueryParams.timeRange,
null,
);
const { queryData: activeLogId } = useUrlQueryData<string | null>(
QueryParams.activeLogId,
null,
);
const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { selectedTime, minTime, maxTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const onTimeRangeChange = useCallback(
(newTimeRange: LogTimeRange | null): void => {
urlQuery.set(QueryParams.timeRange, JSON.stringify(newTimeRange));
if (selectedTime !== 'custom') {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
} else {
urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || '');
urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || '');
urlQuery.delete(QueryParams.relativeTime);
}
@@ -76,14 +68,12 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
event.preventDefault();
event.stopPropagation();
const range = JSON.stringify(timeRange);
urlQuery.delete(QueryParams.activeLogId);
urlQuery.delete(QueryParams.timeRange);
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.activeLogId, `"${logId}"`);
urlQuery.set(QueryParams.timeRange, range);
urlQuery.set(QueryParams.startTime, timeRange?.start.toString() || '');
urlQuery.set(QueryParams.endTime, timeRange?.end.toString() || '');
urlQuery.set(QueryParams.startTime, minTime?.toString() || '');
urlQuery.set(QueryParams.endTime, maxTime?.toString() || '');
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
@@ -92,7 +82,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
message: 'Copied to clipboard',
});
},
[logId, timeRange, urlQuery, pathname, setCopy, notifications],
[logId, urlQuery, minTime, maxTime, pathname, setCopy, notifications],
);
useEffect(() => {
@@ -110,7 +100,6 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
isHighlighted,
isLogsExplorerPage,
activeLogId,
timeRange,
onLogCopy,
onTimeRangeChange,
};

View File

@@ -92,15 +92,9 @@ export const useFetchKeysAndValues = (
const isQueryEnabled = useMemo(
() =>
query.dataSource === DataSource.METRICS
? !!query.aggregateOperator &&
!!query.dataSource &&
!!query.aggregateAttribute.dataType
? !!query.dataSource && !!query.aggregateAttribute.dataType
: true,
[
query.aggregateAttribute.dataType,
query.aggregateOperator,
query.dataSource,
],
[query.aggregateAttribute.dataType, query.dataSource],
);
const { data, isFetching, status } = useGetAggregateKeys(

View File

@@ -7,6 +7,8 @@ import {
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ILog } from 'types/api/logs/log';
import {
IBuilderQuery,
@@ -15,6 +17,7 @@ import {
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import { GlobalReducer } from 'types/reducer/globalTime';
import { LogTimeRange } from './logs/types';
import { useCopyLogLink } from './logs/useCopyLogLink';
@@ -39,6 +42,10 @@ export const useLogsData = ({
const [requestData, setRequestData] = useState<Query | null>(null);
const [shouldLoadMoreLogs, setShouldLoadMoreLogs] = useState<boolean>(false);
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { queryData: pageSize } = useUrlQueryData(
QueryParams.pageSize,
DEFAULT_PER_PAGE_VALUE,
@@ -122,7 +129,7 @@ export const useLogsData = ({
return data;
};
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
const { activeLogId, onTimeRangeChange } = useCopyLogLink();
const { data, isFetching } = useGetExplorerQueryRange(
requestData,
@@ -133,11 +140,10 @@ export const useLogsData = ({
enabled: !isLimit && !!requestData,
},
{
...(timeRange &&
activeLogId &&
...(activeLogId &&
!logs.length && {
start: timeRange.start,
end: timeRange.end,
start: minTime,
end: maxTime,
}),
},
shouldLoadMoreLogs,
@@ -156,7 +162,7 @@ export const useLogsData = ({
setLogs(newLogs);
onTimeRangeChange({
start: currentParams?.start,
end: timeRange?.end || currentParams?.end,
end: currentParams?.end,
pageSize: newLogs.length,
});
}

View File

@@ -53,6 +53,7 @@ export interface GetUPlotChartOptions {
[key: string]: boolean;
}>
>;
customTooltipElement?: HTMLDivElement;
}
/** the function converts series A , series B , series C to
@@ -154,6 +155,7 @@ export const getUPlotChartOptions = ({
stackBarChart: stackChart,
hiddenGraph,
setHiddenGraph,
customTooltipElement,
}: GetUPlotChartOptions): uPlot.Options => {
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
@@ -209,9 +211,16 @@ export const getUPlotChartOptions = ({
},
},
plugins: [
tooltipPlugin({ apiResponse, yAxisUnit, stackBarChart, isDarkMode }),
tooltipPlugin({
apiResponse,
yAxisUnit,
stackBarChart,
isDarkMode,
customTooltipElement,
}),
onClickPlugin({
onClick: onClickHandler,
apiResponse,
}),
],
hooks: {

View File

@@ -1,10 +1,16 @@
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export interface OnClickPluginOpts {
onClick: (
xValue: number,
yValue: number,
mouseX: number,
mouseY: number,
data?: {
[key: string]: string;
},
) => void;
apiResponse?: MetricRangePayloadProps;
}
function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
@@ -22,9 +28,24 @@ function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
const xValue = u.posToVal(event.offsetX, 'x');
const yValue = u.posToVal(event.offsetY, 'y');
opts.onClick(xValue, yValue, mouseX, mouseY);
};
let metric = {};
const { series } = u;
const apiResult = opts.apiResponse?.data?.result || [];
// this is to get the metric value of the focused series
if (Array.isArray(series) && series.length > 0) {
series.forEach((item, index) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (item?.show && item?._focus) {
const { metric: focusedMetric } = apiResult[index - 1] || [];
metric = focusedMetric;
}
});
}
opts.onClick(xValue, yValue, mouseX, mouseY, metric);
};
u.over.addEventListener('click', handleClick);
},
destroy: (u: uPlot) => {

View File

@@ -222,6 +222,7 @@ type ToolTipPluginProps = {
isMergedSeries?: boolean;
stackBarChart?: boolean;
isDarkMode: boolean;
customTooltipElement?: HTMLDivElement;
};
const tooltipPlugin = ({
@@ -232,7 +233,9 @@ const tooltipPlugin = ({
isMergedSeries,
stackBarChart,
isDarkMode,
}: ToolTipPluginProps): any => {
customTooltipElement,
}: // eslint-disable-next-line sonarjs/cognitive-complexity
ToolTipPluginProps): any => {
let over: HTMLElement;
let bound: HTMLElement;
let bLeft: any;
@@ -298,6 +301,9 @@ const tooltipPlugin = ({
isMergedSeries,
stackBarChart,
);
if (customTooltipElement) {
content.appendChild(customTooltipElement);
}
overlay.appendChild(content);
placement(overlay, anchor, 'right', 'start', { bound });
}

View File

@@ -78,13 +78,13 @@ export const explorerView = {
extraData: '{"color":"#00ffd0"}',
},
{
uuid: '58b010b6-8be9-40d1-8d25-f73b5f7314ad',
name: 'success traces list view',
uuid: '8c4bf492-d54d-4ab2-a8d6-9c1563f46e1f',
name: 'R-test panel',
category: '',
createdAt: '2023-08-30T13:00:40.958011925Z',
createdBy: 'test-email',
updatedAt: '2024-04-29T13:09:06.175537361Z',
updatedBy: 'test-email',
createdAt: '2024-07-01T13:45:57.924686766Z',
createdBy: 'test-user-test',
updatedAt: '2024-07-01T13:48:31.032106578Z',
updatedBy: 'test-user-test',
sourcePage: 'traces',
tags: [''],
compositeQuery: {
@@ -106,13 +106,13 @@ export const explorerView = {
items: [
{
key: {
key: 'responseStatusCode',
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
},
value: '200',
value: 'GET',
op: '=',
},
],
@@ -128,7 +128,7 @@ export const explorerView = {
order: 'desc',
},
],
reduceTo: 'sum',
reduceTo: 'avg',
timeAggregation: 'rate',
spaceAggregation: 'sum',
ShiftBy: 0,
@@ -137,7 +137,7 @@ export const explorerView = {
panelType: 'list',
queryType: 'builder',
},
extraData: '{"color":"#bdff9d"}',
extraData: '{"color":"#AD7F58"}',
},
],
};

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@ import { membersResponse } from './__mockdata__/members';
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
import { serviceSuccessResponse } from './__mockdata__/services';
import { topLevelOperationSuccessResponse } from './__mockdata__/top_level_operations';
import { traceDetailResponse } from './__mockdata__/tracedetail';
export const handlers = [
rest.post('http://localhost/api/v3/query_range', (req, res, ctx) =>
@@ -230,6 +231,12 @@ export const handlers = [
}),
),
),
rest.get(
'http://localhost/api/v1/traces/000000000000000071dc9b0a338729b4',
(req, res, ctx) => res(ctx.status(200), ctx.json(traceDetailResponse)),
),
rest.post('http://localhost/api/v1//channels', (_, res, ctx) =>
res(ctx.status(200), ctx.json(allAlertChannels)),
),

View File

@@ -0,0 +1,34 @@
.coming-soon {
display: inline-flex;
padding: 4px 8px;
border-radius: 20px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
justify-content: center;
align-items: center;
gap: 5px;
&__text {
color: var(--text-sienna-400);
font-size: 10px;
font-weight: 500;
letter-spacing: -0.05px;
line-height: normal;
}
&__icon {
display: flex;
}
}
.tooltip-overlay {
text-wrap: nowrap;
.ant-tooltip-inner {
width: max-content;
}
}
.select-label-with-coming-soon {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

@@ -0,0 +1,58 @@
/* eslint-disable react/destructuring-assignment */
import './MQCommon.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { Info } from 'lucide-react';
export function ComingSoon(): JSX.Element {
return (
<Tooltip
title={
<div>
Join our Slack community for more details:{' '}
<a
href="https://signoz.io/slack"
rel="noopener noreferrer"
target="_blank"
onClick={(e): void => e.stopPropagation()}
>
SigNoz Community
</a>
</div>
}
placement="top"
overlayClassName="tooltip-overlay"
>
<div className="coming-soon">
<div className="coming-soon__text">Coming Soon</div>
<div className="coming-soon__icon">
<Info size={10} color={Color.BG_SIENNA_400} />
</div>
</div>
</Tooltip>
);
}
export function SelectMaxTagPlaceholder(
omittedValues: Partial<DefaultOptionType>[],
): JSX.Element {
return (
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>
<span>+ {omittedValues.length} </span>
</Tooltip>
);
}
export function SelectLabelWithComingSoon({
label,
}: {
label: string;
}): JSX.Element {
return (
<div className="select-label-with-coming-soon">
{label} <ComingSoon />
</div>
);
}

View File

@@ -0,0 +1,84 @@
import '../MessagingQueues.styles.scss';
import { Select, Typography } from 'antd';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { ListMinus } from 'lucide-react';
import { useHistory } from 'react-router-dom';
import { MessagingQueuesViewType } from '../MessagingQueuesUtils';
import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon';
import MessagingQueuesDetails from '../MQDetails/MQDetails';
import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions';
import MessagingQueuesGraph from '../MQGraph/MQGraph';
function MQDetailPage(): JSX.Element {
const history = useHistory();
return (
<div className="messaging-queue-container">
<div className="messaging-breadcrumb">
<ListMinus size={16} />
<Typography.Text
onClick={(): void => history.push(ROUTES.MESSAGING_QUEUES)}
className="message-queue-text"
>
Messaging Queues
</Typography.Text>
</div>
<div className="messaging-header">
<div className="header-config">
Kafka / views /
<Select
className="messaging-queue-options"
defaultValue={MessagingQueuesViewType.consumerLag.value}
popupClassName="messaging-queue-options-popup"
options={[
{
label: MessagingQueuesViewType.consumerLag.label,
value: MessagingQueuesViewType.consumerLag.value,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.partitionLatency.label}
/>
),
value: MessagingQueuesViewType.partitionLatency.value,
disabled: true,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.producerLatency.label}
/>
),
value: MessagingQueuesViewType.producerLatency.value,
disabled: true,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.consumerLatency.label}
/>
),
value: MessagingQueuesViewType.consumerLatency.value,
disabled: true,
},
]}
/>
</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<div className="messaging-queue-main-graph">
<MessagingQueuesConfigOptions />
<MessagingQueuesGraph />
</div>
<div className="messaging-queue-details">
<MessagingQueuesDetails />
</div>
</div>
);
}
export default MQDetailPage;

View File

@@ -0,0 +1,3 @@
import MQDetailPage from './MQDetailPage';
export default MQDetailPage;

View File

@@ -0,0 +1,6 @@
.mq-details {
width: 100%;
display: flex;
flex-direction: column;
gap: 24px;
}

View File

@@ -0,0 +1,67 @@
import './MQDetails.style.scss';
import { Radio } from 'antd';
import { Dispatch, SetStateAction, useState } from 'react';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
} from '../MessagingQueuesUtils';
import { ComingSoon } from '../MQCommon/MQCommon';
import MessagingQueuesTable from './MQTables/MQTables';
function MessagingQueuesOptions({
currentTab,
setCurrentTab,
}: {
currentTab: ConsumerLagDetailType;
setCurrentTab: Dispatch<SetStateAction<ConsumerLagDetailType>>;
}): JSX.Element {
const [option, setOption] = useState<ConsumerLagDetailType>(currentTab);
return (
<Radio.Group
onChange={(value): void => {
setOption(value.target.value);
setCurrentTab(value.target.value);
}}
value={option}
className="mq-details-options"
>
<Radio.Button value={ConsumerLagDetailType.ConsumerDetails} checked>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ConsumerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.ProducerDetails}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ProducerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.NetworkLatency}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.NetworkLatency]}
</Radio.Button>
<Radio.Button
value={ConsumerLagDetailType.PartitionHostMetrics}
disabled
className="disabled-option"
>
{ConsumerLagDetailTitle[ConsumerLagDetailType.PartitionHostMetrics]}
<ComingSoon />
</Radio.Button>
</Radio.Group>
);
}
function MessagingQueuesDetails(): JSX.Element {
const [currentTab, setCurrentTab] = useState<ConsumerLagDetailType>(
ConsumerLagDetailType.ConsumerDetails,
);
return (
<div className="mq-details">
<MessagingQueuesOptions
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
<MessagingQueuesTable currentTab={currentTab} />
</div>
);
}
export default MessagingQueuesDetails;

View File

@@ -0,0 +1,99 @@
.mq-tables-container {
.mq-table-title {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 16px;
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 28px;
letter-spacing: -0.09px;
.mq-table-subtitle {
color: var(--bg-vanilla-400);
font-size: 14px;
}
}
.mq-table {
width: 100%;
.ant-table-content {
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
}
.ant-table-tbody {
.ant-table-cell {
max-width: 250px;
background-color: var(--bg-ink-400);
border-bottom: none;
}
}
.ant-table-thead {
.ant-table-cell {
background-color: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-500);
}
}
}
.no-data-style {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex-direction: column;
gap: 24px;
padding: 24px;
border: 1px solid var(--bg-slate-500);
border-radius: 6px;
.ant-typography {
font-size: 14px;
}
}
}
.lightMode {
.mq-tables-container {
.mq-table-title {
color: var(--bg-slate-200);
.mq-table-subtitle {
color: var(--bg-slate-300);
}
}
.mq-table {
.ant-table-content {
border: 1px solid var(--bg-vanilla-300);
}
.ant-table-tbody {
.ant-table-cell {
background-color: var(--bg-vanilla-100);
}
}
.ant-table-thead {
.ant-table-cell {
background-color: var(--bg-vanilla-100);
border-bottom: 1px solid var(--bg-vanilla-300);
}
}
}
.no-data-style {
border: 1px solid var(--bg-vanilla-300);
}
}
}

View File

@@ -0,0 +1,210 @@
import './MQTables.styles.scss';
import { Skeleton, Table, Typography } from 'antd';
import axios from 'axios';
import { isNumber } from 'chart.js/helpers';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { History } from 'history';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
convertToTitleCase,
RowData,
SelectedTimelineQuery,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useHistory } from 'react-router-dom';
import {
ConsumerLagPayload,
getConsumerLagDetails,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
// eslint-disable-next-line sonarjs/cognitive-complexity
export function getColumns(
data: MessagingQueuesPayloadProps['payload'],
history: History<unknown>,
): RowData[] {
console.log(data);
if (data?.result?.length === 0) {
return [];
}
const columns: {
title: string;
dataIndex: string;
key: string;
}[] = data?.result?.[0]?.table?.columns.map((column) => ({
title: convertToTitleCase(column.name),
dataIndex: column.name,
key: column.name,
render: [
'p99',
'error_rate',
'throughput',
'avg_msg_size',
'error_percentage',
].includes(column.name)
? (value: number | string): string => {
if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
}
: (text: string): ColumnTypeRender<Record<string, unknown>> => ({
children:
column.name === 'service_name' ? (
<Typography.Link
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
history.push(`/services/${encodeURIComponent(text)}`);
}}
>
{text}
</Typography.Link>
) : (
<Typography.Text>{text}</Typography.Text>
),
}),
}));
return columns;
}
export function getTableData(
data: MessagingQueuesPayloadProps['payload'],
): RowData[] {
if (data?.result?.length === 0) {
return [];
}
const tableData: RowData[] =
data?.result?.[0]?.table?.rows?.map(
(row, index: number): RowData => ({
...row.data,
key: index,
}),
) || [];
return tableData;
}
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
<>
<Typography.Text className="numbers">
{range[0]} &#8212; {range[1]}
</Typography.Text>
<Typography.Text className="total"> of {total}</Typography.Text>
</>
);
function MessagingQueuesTable({
currentTab,
}: {
currentTab: ConsumerLagDetailType;
}): JSX.Element {
const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]);
const { notifications } = useNotifications();
const urlQuery = useUrlQuery();
const history = useHistory();
const timelineQuery = decodeURIComponent(
urlQuery.get(QueryParams.selectedTimelineQuery) || '',
);
const timelineQueryData: SelectedTimelineQuery = useMemo(
() => (timelineQuery ? JSON.parse(timelineQuery) : {}),
[timelineQuery],
);
const paginationConfig = useMemo(
() =>
tableData?.length > 20 && {
pageSize: 20,
showTotal: showPaginationItem,
showSizeChanger: false,
hideOnSinglePage: true,
},
[tableData],
);
const props: ConsumerLagPayload = useMemo(
() => ({
start: (timelineQueryData?.start || 0) * 1e9,
end: (timelineQueryData?.end || 0) * 1e9,
variables: {
partition: timelineQueryData?.partition,
topic: timelineQueryData?.topic,
consumer_group: timelineQueryData?.group,
},
detailType: currentTab,
}),
[currentTab, timelineQueryData],
);
const handleConsumerDetailsOnError = (error: Error): void => {
notifications.error({
message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
});
};
const { mutate: getConsumerDetails, isLoading } = useMutation(
getConsumerLagDetails,
{
onSuccess: (data) => {
if (data.payload) {
setColumns(getColumns(data?.payload, history));
setTableData(getTableData(data?.payload));
}
},
onError: handleConsumerDetailsOnError,
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => getConsumerDetails(props), [currentTab, props]);
const isEmptyDetails = (timelineQueryData: SelectedTimelineQuery): boolean =>
isEmpty(timelineQueryData) ||
(!timelineQueryData?.group &&
!timelineQueryData?.topic &&
!timelineQueryData?.partition);
return (
<div className="mq-tables-container">
{isEmptyDetails(timelineQueryData) ? (
<div className="no-data-style">
<Typography.Text>
Click on a co-ordinate above to see the details
</Typography.Text>
<Skeleton />
</div>
) : (
<>
<div className="mq-table-title">
{ConsumerLagDetailTitle[currentTab]}
<div className="mq-table-subtitle">{`${timelineQueryData?.group || ''} ${
timelineQueryData?.topic || ''
} ${timelineQueryData?.partition || ''}`}</div>
</div>
<Table
className="mq-table"
pagination={paginationConfig}
size="middle"
columns={columns}
dataSource={tableData}
bordered={false}
loading={isLoading}
/>
</>
)}
</div>
);
}
export default MessagingQueuesTable;

View File

@@ -0,0 +1,61 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ConsumerLagDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface ConsumerLagPayload {
start?: number | string;
end?: number | string;
variables: {
partition?: string;
topic?: string;
consumer_group?: string;
};
detailType: ConsumerLagDetailType;
}
export interface MessagingQueuesPayloadProps {
status: string;
payload: {
resultType: string;
result: {
table: {
columns: {
name: string;
queryName: string;
isValueColumn: boolean;
}[];
rows: {
data: Record<string, string>;
}[];
};
}[];
};
}
export const getConsumerLagDetails = async (
props: ConsumerLagPayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, ...restProps } = props;
try {
const response = await axios.post(
`/messaging-queues/kafka/consumer-lag/${props.detailType}`,
{
...restProps,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};

View File

@@ -0,0 +1,4 @@
.mq-config {
display: flex;
justify-content: space-between;
}

View File

@@ -0,0 +1,235 @@
import './MQConfigOptions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Spin, Tooltip } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { QueryParams } from 'constants/query';
import { History, Location } from 'history';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, Share2 } from 'lucide-react';
import { useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { SelectMaxTagPlaceholder } from '../MQCommon/MQCommon';
import { useGetAllConfigOptions } from './useGetAllConfigOptions';
type ConfigOptionType = 'group' | 'topic' | 'partition';
const getPlaceholder = (type: ConfigOptionType): string => {
switch (type) {
case 'group':
return 'Consumer Groups';
case 'topic':
return 'Topics';
case 'partition':
return 'Partitions';
default:
return '';
}
};
const useConfigOptions = (
type: ConfigOptionType,
): {
searchText: string;
handleSearch: (value: string) => void;
isFetching: boolean;
options: DefaultOptionType[];
} => {
const [searchText, setSearchText] = useState<string>('');
const { isFetching, options } = useGetAllConfigOptions({
attributeKey: type,
searchText,
});
const handleDebouncedSearch = useDebouncedFn((searchText): void => {
setSearchText(searchText as string);
}, 500);
const handleSearch = (value: string): void => {
handleDebouncedSearch(value || '');
};
return { searchText, handleSearch, isFetching, options };
};
function setQueryParamsForConfigOptions(
value: string[],
urlQuery: URLSearchParams,
history: History<unknown>,
location: Location<unknown>,
queryParams: QueryParams,
): void {
urlQuery.set(queryParams, value.join(','));
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
function getConfigValuesFromQueryParams(
queryParams: QueryParams,
urlQuery: URLSearchParams,
): string[] {
const value = urlQuery.get(queryParams);
return value ? value.split(',') : [];
}
function MessagingQueuesConfigOptions(): JSX.Element {
const urlQuery = useUrlQuery();
const location = useLocation();
const history = useHistory();
const resetTabularConfigDetailsOnChange = (): void => {
urlQuery.delete(QueryParams.selectedTimelineQuery);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
};
const {
handleSearch: handleConsumerGrpSearch,
isFetching: isFetchingConsumerGrp,
options: consumerGrpOptions,
} = useConfigOptions('group');
const {
handleSearch: handleTopicSearch,
isFetching: isFetchingTopic,
options: topicOptions,
} = useConfigOptions('topic');
const {
handleSearch: handlePartitionSearch,
isFetching: isFetchingPartition,
options: partitionOptions,
} = useConfigOptions('partition');
const [isURLCopied, setIsURLCopied] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
return (
<div className="mq-config">
<div className="config-options">
<Select
placeholder={getPlaceholder('group')}
showSearch
mode="multiple"
options={consumerGrpOptions}
loading={isFetchingConsumerGrp}
className="config-select-option"
onSearch={handleConsumerGrpSearch}
maxTagCount={4}
maxTagPlaceholder={SelectMaxTagPlaceholder}
value={
getConfigValuesFromQueryParams(QueryParams.consumerGrp, urlQuery) || []
}
notFoundContent={
isFetchingConsumerGrp ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No Consumer Groups found</span>
)
}
onChange={(value): void => {
handleConsumerGrpSearch('');
setQueryParamsForConfigOptions(
value,
urlQuery,
history,
location,
QueryParams.consumerGrp,
);
resetTabularConfigDetailsOnChange();
}}
/>
<Select
placeholder={getPlaceholder('topic')}
showSearch
mode="multiple"
options={topicOptions}
loading={isFetchingTopic}
onSearch={handleTopicSearch}
className="config-select-option"
maxTagCount={4}
value={getConfigValuesFromQueryParams(QueryParams.topic, urlQuery) || []}
maxTagPlaceholder={SelectMaxTagPlaceholder}
notFoundContent={
isFetchingTopic ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No Topics found</span>
)
}
onChange={(value): void => {
handleTopicSearch('');
setQueryParamsForConfigOptions(
value,
urlQuery,
history,
location,
QueryParams.topic,
);
resetTabularConfigDetailsOnChange();
}}
/>
<Select
placeholder={getPlaceholder('partition')}
showSearch
mode="multiple"
options={partitionOptions}
loading={isFetchingPartition}
className="config-select-option"
onSearch={handlePartitionSearch}
maxTagCount={4}
value={
getConfigValuesFromQueryParams(QueryParams.partition, urlQuery) || []
}
maxTagPlaceholder={SelectMaxTagPlaceholder}
notFoundContent={
isFetchingPartition ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No Partitions found</span>
)
}
onChange={(value): void => {
handlePartitionSearch('');
setQueryParamsForConfigOptions(
value,
urlQuery,
history,
location,
QueryParams.partition,
);
resetTabularConfigDetailsOnChange();
}}
/>
</div>
<Tooltip title="Share this" arrow={false}>
<Button
className="periscope-btn copy-url-btn"
onClick={(): void => {
handleCopyToClipboard(window.location.href);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
}}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Share2 size={14} />
)
}
/>
</Tooltip>
</div>
);
}
export default MessagingQueuesConfigOptions;

View File

@@ -0,0 +1,88 @@
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import {
getFiltersFromConfigOptions,
getWidgetQuery,
setSelectedTimelineQuery,
} from '../MessagingQueuesUtils';
function MessagingQueuesGraph(): JSX.Element {
const isDarkMode = useIsDarkMode();
const urlQuery = useUrlQuery();
const consumerGrp = urlQuery.get(QueryParams.consumerGrp) || '';
const topic = urlQuery.get(QueryParams.topic) || '';
const partition = urlQuery.get(QueryParams.partition) || '';
const filterItems = useMemo(
() => getFiltersFromConfigOptions(consumerGrp, topic, partition),
[consumerGrp, topic, partition],
);
const widgetData = useMemo(
() => getWidgetQueryBuilder(getWidgetQuery({ filterItems })),
[filterItems],
);
const history = useHistory();
const location = useLocation();
const messagingQueueCustomTooltipText = (): HTMLDivElement => {
const customText = document.createElement('div');
customText.textContent = 'Click on co-ordinate to view details';
customText.style.paddingTop = '8px';
customText.style.paddingBottom = '2px';
customText.style.color = '#fff';
return customText;
};
const { pathname } = useLocation();
const dispatch = useDispatch();
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
);
return (
<Card
isDarkMode={isDarkMode}
$panelType={PANEL_TYPES.TIME_SERIES}
className="mq-graph"
>
<GridCard
widget={widgetData}
headerMenuList={[...ViewMenuAction]}
onClickHandler={(xValue, _yValue, _mouseX, _mouseY, data): void => {
setSelectedTimelineQuery(urlQuery, xValue, location, history, data);
}}
onDragSelect={onDragSelect}
customTooltipElement={messagingQueueCustomTooltipText()}
/>
</Card>
);
}
export default MessagingQueuesGraph;

View File

@@ -0,0 +1,49 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { DefaultOptionType } from 'antd/es/select';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { useQuery } from 'react-query';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
export interface ConfigOptions {
attributeKey: string;
searchText?: string;
}
export interface GetAllConfigOptionsResponse {
options: DefaultOptionType[];
isFetching: boolean;
}
export function useGetAllConfigOptions(
props: ConfigOptions,
): GetAllConfigOptionsResponse {
const { attributeKey, searchText } = props;
const { data, isLoading } = useQuery(
['attributesValues', attributeKey, searchText],
async () => {
const { payload } = await getAttributesValues({
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
aggregateAttribute: 'kafka_consumer_group_lag',
attributeKey,
searchText: searchText ?? '',
filterAttributeKeyDataType: DataTypes.String,
tagType: 'tag',
});
if (payload) {
const values = Object.values(payload).find((el) => !!el) || [];
const options: DefaultOptionType[] = values.map((val: string) => ({
label: val,
value: val,
}));
return options;
}
return [];
},
);
return { options: data ?? [], isFetching: isLoading };
}

View File

@@ -0,0 +1,424 @@
.messaging-queue-container {
.messaging-breadcrumb {
display: flex;
padding: 0px 16px;
align-items: center;
gap: 8px;
padding-top: 10px;
padding-bottom: 8px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
border-bottom: 1px solid var(--bg-slate-400);
.message-queue-text {
cursor: pointer;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
}
.messaging-header {
display: flex;
min-height: 48px;
padding: 10px 16px;
justify-content: space-between;
align-items: center;
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px;
border-bottom: 1px solid var(--bg-slate-500);
.header-config {
display: flex;
gap: 10px;
align-items: center;
.messaging-queue-options {
.ant-select-selector {
display: flex;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
}
}
.messaging-queue-main-graph {
display: flex;
padding: 24px 16px;
flex-direction: column;
gap: 16px;
.config-options {
display: flex;
align-items: center;
gap: 8px;
.config-select-option {
.ant-select-selector {
display: flex;
min-height: 32px;
align-items: center;
gap: 16px;
min-width: 164px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
}
.mq-graph {
height: 420px;
padding: 24px 24px 0 24px;
}
border-bottom: 1px solid var(--bg-slate-500);
}
.messaging-queue-details {
display: flex;
padding: 16px;
.mq-details-options {
letter-spacing: -0.06px;
.ant-radio-button-wrapper {
border-color: var(--bg-slate-400);
color: var(--bg-vanilla-400);
}
.ant-radio-button-wrapper-checked {
background: var(--bg-slate-400);
color: var(--bg-vanilla-100);
}
.ant-radio-button-wrapper-disabled {
background: var(--bg-ink-400);
color: var(--bg-slate-200);
}
.ant-radio-button-wrapper::before {
width: 0px;
}
.disabled-option {
.coming-soon {
margin-left: 8px;
}
}
}
}
}
.messaging-queue-options-popup {
width: 264px !important;
}
.messaging-overview {
padding: 24px 16px 10px 16px;
.overview-text {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.08px;
margin: 0;
}
.overview-subtext {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
margin: 0;
margin-top: 4px;
}
.overview-doc-area {
margin: 16px 0 28px 0;
display: flex;
.middle-card {
border-left: none !important;
border-right: none !important;
}
.overview-info-card {
display: flex;
width: 376px;
min-height: 176px;
padding: 18px 20px 20px 20px;
flex-direction: column;
justify-content: space-between;
border: 1px solid var(--bg-slate-500);
border-radius: 2px;
.card-title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
margin: 0;
}
.card-info-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
margin: 0;
margin-top: 4px;
}
.button-grp {
display: flex;
gap: 8px;
.ant-btn {
min-width: 80px;
}
.ant-btn-default {
background-color: var(--bg-slate-400);
border: none;
box-shadow: none;
}
}
}
}
.summary-section {
display: flex;
.summary-card {
display: flex;
padding: 12px;
flex-direction: column;
align-items: flex-start;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
width: 337px;
height: 283px;
border-radius: 2px;
.summary-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 24px;
> p {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500; /* 169.231% */
letter-spacing: 0.52px;
text-transform: uppercase;
margin: 0;
}
.time-value {
display: flex;
gap: 4px;
align-items: center;
> p {
color: var(--bg-slate-200);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 22px;
letter-spacing: 0.48px;
text-transform: uppercase;
}
}
}
.view-detail-btn {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.coming-soon-card {
background: var(--bg-ink-500) !important;
border-left: none !important;
}
}
.overview-confirm-modal {
background-color: var(--bg-ink-500);
padding: 0;
border-radius: 4px;
.ant-modal-content {
background-color: var(--bg-ink-300);
.ant-modal-confirm-content {
color: var(--bg-vanilla-100);
}
.ant-modal-confirm-body-wrapper {
display: flex;
flex-direction: column;
height: 150px;
justify-content: space-between;
}
}
}
.lightMode {
.messaging-queue-container {
.messaging-breadcrumb {
color: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-vanilla-300);
}
.messaging-header {
color: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-vanilla-300);
.header-config {
.messaging-queue-options {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
}
.messaging-queue-main-graph {
.config-options {
.config-select-option {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
border-bottom: 1px solid var(--bg-vanilla-300);
}
.messaging-queue-details {
.mq-details-options {
.ant-radio-button-wrapper {
border-color: var(--bg-vanilla-300);
color: var(--bg-slate-200);
}
.ant-radio-button-wrapper-checked {
color: var(--bg-slate-200);
background: var(--bg-vanilla-300);
}
.ant-radio-button-wrapper-disabled {
background: var(--bg-vanilla-100);
color: var(--bg-vanilla-400);
}
}
}
}
.messaging-overview {
.overview-text {
color: var(--bg-slate-200);
}
.overview-subtext {
color: var(--bg-slate-300);
}
.overview-doc-area {
.overview-info-card {
border: 1px solid var(--bg-vanilla-300);
.card-title {
color: var(--bg-slate-200);
}
.card-info-text {
color: var(--bg-slate-300);
}
.button-grp {
.ant-btn-default {
background-color: var(--bg-vanilla-100);
}
}
}
}
.summary-section {
.summary-card {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.summary-title {
> p {
color: var(--bg-slate-200);
}
.time-value {
> p {
color: var(--bg-slate-200);
}
}
}
}
}
.coming-soon-card {
background: var(--bg-vanilla-200) !important;
}
}
.overview-confirm-modal {
background-color: var(--bg-vanilla-100);
.ant-modal-content {
background-color: var(--bg-vanilla-100);
.ant-modal-confirm-content {
color: var(--bg-slate-200);
}
}
}
}

View File

@@ -0,0 +1,174 @@
import './MessagingQueues.styles.scss';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal } from 'antd';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Calendar, ListMinus } from 'lucide-react';
import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app';
import {
KAFKA_SETUP_DOC_LINK,
MessagingQueuesViewType,
} from './MessagingQueuesUtils';
import { ComingSoon } from './MQCommon/MQCommon';
function MessagingQueues(): JSX.Element {
const history = useHistory();
const { confirm } = Modal;
const showConfirm = (): void => {
confirm({
icon: <ExclamationCircleFilled />,
content:
'Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.',
className: 'overview-confirm-modal',
onOk() {
history.push(ROUTES.MESSAGING_QUEUES_DETAIL);
},
okText: 'Proceed',
});
};
const isCloudUserVal = isCloudUser();
const getStartedRedirect = (link: string): void => {
if (isCloudUserVal) {
history.push(link);
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
};
return (
<div className="messaging-queue-container">
<div className="messaging-breadcrumb">
<ListMinus size={16} />
Messaging Queues
</div>
<div className="messaging-header">
<div className="header-config">Kafka / Overview</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<div className="messaging-overview">
<p className="overview-text">
Start sending data in as little as 20 minutes
</p>
<p className="overview-subtext">Connect and Monitor Your Data Streams</p>
<div className="overview-doc-area">
<div className="overview-info-card">
<div>
<p className="card-title">Configure Consumer</p>
<p className="card-info-text">
Connect your consumer and producer data sources to start monitoring.
</p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
getStartedRedirect(ROUTES.GET_STARTED_APPLICATION_MONITORING)
}
>
Get Started
</Button>
</div>
</div>
<div className="overview-info-card middle-card">
<div>
<p className="card-title">Configure Producer</p>
<p className="card-info-text">
Connect your consumer and producer data sources to start monitoring.
</p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
getStartedRedirect(ROUTES.GET_STARTED_APPLICATION_MONITORING)
}
>
Get Started
</Button>
</div>
</div>
<div className="overview-info-card">
<div>
<p className="card-title">Monitor kafka</p>
<p className="card-info-text">
Set up your Kafka monitoring to track consumer and producer activities.
</p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
getStartedRedirect(ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING)
}
>
Get Started
</Button>
</div>
</div>
</div>
<div className="summary-section">
<div className="summary-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.consumerLag.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
</div>
<div className="view-detail-btn">
<Button type="primary" onClick={showConfirm}>
View Details
</Button>
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.partitionLatency.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
</div>
<div className="view-detail-btn">
<ComingSoon />
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.producerLatency.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
</div>
<div className="view-detail-btn">
<ComingSoon />
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.consumerLatency.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
</div>
<div className="view-detail-btn">
<ComingSoon />
</div>
</div>
</div>
</div>
</div>
);
}
export default MessagingQueues;

View File

@@ -0,0 +1,225 @@
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
import { History, Location } from 'history';
import { isEmpty } from 'lodash-es';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
export const KAFKA_SETUP_DOC_LINK =
'https://github.com/shivanshuraj1333/kafka-opentelemetry-instrumentation/tree/master';
export function convertToTitleCase(text: string): string {
return text
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}
export type RowData = {
key: string | number;
[key: string]: string | number;
};
export enum ConsumerLagDetailType {
ConsumerDetails = 'consumer-details',
ProducerDetails = 'producer-details',
NetworkLatency = 'network-latency',
PartitionHostMetrics = 'partition-host-metric',
}
export const ConsumerLagDetailTitle: Record<ConsumerLagDetailType, string> = {
'consumer-details': 'Consumer Groups Details',
'producer-details': 'Producer Details',
'network-latency': 'Network Latency',
'partition-host-metric': 'Partition Host Metrics',
};
export function createWidgetFilterItem(
key: string,
value: string,
): TagFilterItem {
const id = `${key}--string--tag--false`;
return {
id: uuid(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id,
},
op: '=',
value,
};
}
export function getFiltersFromConfigOptions(
consumerGrp?: string,
topic?: string,
partition?: string,
): TagFilterItem[] {
const configOptions = [
{ key: 'group', values: consumerGrp?.split(',') },
{ key: 'topic', values: topic?.split(',') },
{ key: 'partition', values: partition?.split(',') },
];
return configOptions.reduce<TagFilterItem[]>(
(accumulator, { key, values }) => {
if (values && !isEmpty(values.filter((item) => item !== ''))) {
accumulator.push(
...values.map((value) => createWidgetFilterItem(key, value)),
);
}
return accumulator;
},
[],
);
}
export function getWidgetQuery({
filterItems,
}: {
filterItems: TagFilterItem[];
}): GetWidgetQueryBuilderProps {
return {
title: 'Consumer Lag',
panelTypes: PANEL_TYPES.TIME_SERIES,
fillSpans: false,
yAxisUnit: 'none',
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'kafka_consumer_group_lag--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'kafka_consumer_group_lag',
type: 'Gauge',
},
aggregateOperator: 'max',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: filterItems || [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'group--string--tag--false',
isColumn: false,
isJSON: false,
key: 'group',
type: 'tag',
},
{
dataType: DataTypes.String,
id: 'topic--string--tag--false',
isColumn: false,
isJSON: false,
key: 'topic',
type: 'tag',
},
{
dataType: DataTypes.String,
id: 'partition--string--tag--false',
isColumn: false,
isJSON: false,
key: 'partition',
type: 'tag',
},
],
having: [],
legend: '{{group}}-{{topic}}-{{partition}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'avg',
stepInterval: 60,
timeAggregation: 'max',
},
],
queryFormulas: [],
},
clickhouse_sql: [],
id: uuid(),
},
};
}
export const convertToNanoseconds = (timestamp: number): bigint =>
BigInt((timestamp * 1e9).toFixed(0));
export const getStartAndEndTimesInMilliseconds = (
timestamp: number,
): { start: number; end: number } => {
const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; // 5 minutes in milliseconds - check with Shivanshu once
const start = Math.floor(timestamp);
const end = Math.floor(start + FIVE_MINUTES_IN_MILLISECONDS);
return { start, end };
};
export interface SelectedTimelineQuery {
group?: string;
partition?: string;
topic?: string;
start?: number;
end?: number;
}
export function setSelectedTimelineQuery(
urlQuery: URLSearchParams,
timestamp: number,
location: Location<unknown>,
history: History<unknown>,
data?: {
[key: string]: string;
},
): void {
const selectedTimelineQuery: SelectedTimelineQuery = {
group: data?.group,
partition: data?.partition,
topic: data?.topic,
...getStartAndEndTimesInMilliseconds(timestamp),
};
urlQuery.set(
QueryParams.selectedTimelineQuery,
encodeURIComponent(JSON.stringify(selectedTimelineQuery)),
);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
export const MessagingQueuesViewType = {
consumerLag: {
label: 'Consumer Lag view',
value: 'consumerLag',
},
partitionLatency: {
label: 'Partition Latency view',
value: 'partitionLatency',
},
producerLatency: {
label: 'Producer Latency view',
value: 'producerLatency',
},
consumerLatency: {
label: 'Consumer latency view',
value: 'consumerLatency',
},
};

View File

@@ -0,0 +1,3 @@
import MessagingQueues from './MessagingQueues';
export default MessagingQueues;

View File

@@ -0,0 +1,172 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable sonarjs/no-duplicate-string */
import ROUTES from 'constants/routes';
import { explorerView } from 'mocks-server/__mockdata__/explorer_views';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { MemoryRouter, Route } from 'react-router-dom';
import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils';
import SaveView from '..';
const handleExplorerTabChangeTest = jest.fn();
jest.mock('hooks/useHandleExplorerTabChange', () => ({
useHandleExplorerTabChange: () => ({
handleExplorerTabChange: handleExplorerTabChangeTest,
}),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({
pathname: `${ROUTES.TRACES_SAVE_VIEWS}`,
}),
}));
describe('SaveView', () => {
it('should render the SaveView component', async () => {
render(<SaveView />);
expect(await screen.findByText('Table View')).toBeInTheDocument();
const savedViews = screen.getAllByRole('row');
expect(savedViews).toHaveLength(2);
// assert row 1
expect(
within(document.querySelector('.view-tag') as HTMLElement).getByText('T'),
).toBeInTheDocument();
expect(screen.getByText('test-user-1')).toBeInTheDocument();
// assert row 2
expect(screen.getByText('R-test panel')).toBeInTheDocument();
expect(screen.getByText('test-user-test')).toBeInTheDocument();
});
it('explorer icon should take the user to the related explorer page', async () => {
render(
<MemoryRouter initialEntries={[ROUTES.TRACES_SAVE_VIEWS]}>
<Route path={ROUTES.TRACES_SAVE_VIEWS}>
<SaveView />
</Route>
</MemoryRouter>,
);
expect(await screen.findByText('Table View')).toBeInTheDocument();
const explorerIcon = await screen.findAllByTestId('go-to-explorer');
expect(explorerIcon[0]).toBeInTheDocument();
// Simulate click on explorer icon
fireEvent.click(explorerIcon[0]);
await waitFor(() =>
expect(handleExplorerTabChangeTest).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
'/traces-explorer', // Asserts the third argument is '/traces-explorer'
),
);
});
it('should render the SaveView component with a search input', async () => {
render(<SaveView />);
const searchInput = screen.getByPlaceholderText('Search for views...');
expect(await screen.findByText('Table View')).toBeInTheDocument();
expect(searchInput).toBeInTheDocument();
// search for 'R-test panel'
searchInput.focus();
(searchInput as HTMLInputElement).setSelectionRange(
0,
(searchInput as HTMLInputElement).value.length,
);
fireEvent.change(searchInput, { target: { value: 'R-test panel' } });
expect(searchInput).toHaveValue('R-test panel');
searchInput.blur();
expect(await screen.findByText('R-test panel')).toBeInTheDocument();
// Table View should not be present now
const savedViews = screen.getAllByRole('row');
expect(savedViews).toHaveLength(1);
});
it('should be able to edit name of view', async () => {
server.use(
rest.put(
'http://localhost/api/v1/explorer/views/test-uuid-1',
// eslint-disable-next-line no-return-assign
(_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
...explorerView,
data: [
...explorerView.data,
(explorerView.data[0].name = 'New Table View'),
],
}),
),
),
);
render(<SaveView />);
const editButton = await screen.findAllByTestId('edit-view');
fireEvent.click(editButton[0]);
const viewName = await screen.findByTestId('view-name');
expect(viewName).toBeInTheDocument();
expect(viewName).toHaveValue('Table View');
const newViewName = 'New Table View';
fireEvent.change(viewName, { target: { value: newViewName } });
expect(viewName).toHaveValue(newViewName);
const saveButton = await screen.findByTestId('save-view');
fireEvent.click(saveButton);
await waitFor(() =>
expect(screen.getByText(newViewName)).toBeInTheDocument(),
);
});
it('should be able to delete a view', async () => {
server.use(
rest.delete(
'http://localhost/api/v1/explorer/views/test-uuid-1',
(_req, res, ctx) => res(ctx.status(200), ctx.json({ status: 'success' })),
),
);
render(<SaveView />);
const deleteButton = await screen.findAllByTestId('delete-view');
fireEvent.click(deleteButton[0]);
expect(await screen.findByText('delete_confirm_message')).toBeInTheDocument();
const confirmButton = await screen.findByTestId('confirm-delete');
fireEvent.click(confirmButton);
await waitFor(() => expect(screen.queryByText('Table View')).toBeNull());
});
it('should render empty state', async () => {
server.use(
rest.get('http://localhost/api/v1/explorer/views', (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: [],
}),
),
),
);
render(<SaveView />);
expect(screen.getByText('No data')).toBeInTheDocument();
});
});

View File

@@ -263,13 +263,19 @@ function SaveView(): JSX.Element {
<PenLine
size={14}
className={isEditDeleteSupported ? '' : 'hidden'}
data-testid="edit-view"
onClick={(): void => handleEditModelOpen(view, bgColor)}
/>
<Compass size={14} onClick={(): void => handleRedirectQuery(view)} />
<Compass
size={14}
onClick={(): void => handleRedirectQuery(view)}
data-testid="go-to-explorer"
/>
<Trash2
size={14}
className={isEditDeleteSupported ? '' : 'hidden'}
color={Color.BG_CHERRY_500}
data-testid="delete-view"
onClick={(): void => handleDeleteModelOpen(view.uuid, view.name)}
/>
</div>
@@ -347,6 +353,7 @@ function SaveView(): JSX.Element {
onClick={onDeleteHandler}
className="delete-btn"
disabled={isDeleteLoading}
data-testid="confirm-delete"
>
Delete view
</Button>,
@@ -371,6 +378,7 @@ function SaveView(): JSX.Element {
icon={<Check size={16} color={Color.BG_VANILLA_100} />}
onClick={onUpdateQueryHandler}
disabled={isViewUpdating}
data-testid="save-view"
>
Save changes
</Button>,
@@ -385,6 +393,7 @@ function SaveView(): JSX.Element {
<Input
placeholder="e.g. Crash landing view"
value={newViewName}
data-testid="view-name"
onChange={(e): void => setNewViewName(e.target.value)}
/>
</div>

View File

@@ -0,0 +1,214 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import ROUTES from 'constants/routes';
import { MemoryRouter, Route } from 'react-router-dom';
import { fireEvent, render, screen } from 'tests/test-utils';
import TraceDetail from '..';
window.HTMLElement.prototype.scrollIntoView = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string; search: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACE_DETAIL}`,
search: '?spanId=28a8a67365d0bd8b&levelUp=0&levelDown=0',
}),
useParams: jest.fn().mockReturnValue({
id: '000000000000000071dc9b0a338729b4',
}),
}));
jest.mock('container/TraceFlameGraph/index.tsx', () => ({
__esModule: true,
default: (): JSX.Element => <div>TraceFlameGraph</div>,
}));
describe('TraceDetail', () => {
it('should render tracedetail', async () => {
const { findByText, getByText, getAllByText, getByPlaceholderText } = render(
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
<Route path={ROUTES.TRACE_DETAIL}>
<TraceDetail />
</Route>
,
</MemoryRouter>,
);
expect(await findByText('Trace Details')).toBeInTheDocument();
// as we have an active spanId, it should scroll to the selected span
expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled();
// assertions
expect(getByText('TraceFlameGraph')).toBeInTheDocument();
expect(getByText('Focus on selected span')).toBeInTheDocument();
// span action buttons
expect(getByText('Reset Focus')).toBeInTheDocument();
expect(getByText('50 Spans')).toBeInTheDocument();
// trace span detail - parent -> child
expect(getAllByText('frontend')[0]).toBeInTheDocument();
expect(getByText('776.76 ms')).toBeInTheDocument();
[
{ trace: 'HTTP GET /dispatch', duration: '776.76 ms', count: '50' },
{ trace: 'HTTP GET: /customer', duration: '349.44 ms', count: '4' },
{
trace: '/driver.DriverService/FindNearest',
duration: '173.10 ms',
count: '15',
},
// and so on ...
].forEach((traceDetail) => {
expect(getByText(traceDetail.trace)).toBeInTheDocument();
expect(getByText(traceDetail.duration)).toBeInTheDocument();
expect(getByText(traceDetail.count)).toBeInTheDocument();
});
// Details for selected Span
expect(getByText('Details for selected Span')).toBeInTheDocument();
['Service', 'Operation', 'SpanKind', 'StatusCodeString'].forEach((detail) => {
expect(getByText(detail)).toBeInTheDocument();
});
// go to related logs button
const goToRelatedLogsButton = getByText('Go to Related logs');
expect(goToRelatedLogsButton).toBeInTheDocument();
// Tag and Event tabs
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Events')).toBeInTheDocument();
expect(getByPlaceholderText('traceDetails:search_tags')).toBeInTheDocument();
// Tag details
[
{ title: 'client-uuid', value: '64a18ffd5f8adbfb' },
{ title: 'component', value: 'net/http' },
{ title: 'host.name', value: '4f6ec470feea' },
{ title: 'http.method', value: 'GET' },
{ title: 'http.url', value: '/route?dropoff=728%2C326&pickup=165%2C543' },
{ title: 'http.status_code', value: '200' },
{ title: 'ip', value: '172.25.0.2' },
{ title: 'opencensus.exporterversion', value: 'Jaeger-Go-2.30.0' },
].forEach((tag) => {
expect(getByText(tag.title)).toBeInTheDocument();
expect(getByText(tag.value)).toBeInTheDocument();
});
// see full value
expect(getAllByText('View full value')[0]).toBeInTheDocument();
});
it('should render tracedetail events tab', async () => {
const { findByText, getByText } = render(
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
<Route path={ROUTES.TRACE_DETAIL}>
<TraceDetail />
</Route>
,
</MemoryRouter>,
);
expect(await findByText('Trace Details')).toBeInTheDocument();
fireEvent.click(getByText('Events'));
expect(await screen.findByText('HTTP request received')).toBeInTheDocument();
// event details
[
{ title: 'Event Start Time', value: '527.60 ms' },
{ title: 'level', value: 'info' },
].forEach((tag) => {
expect(getByText(tag.title)).toBeInTheDocument();
expect(getByText(tag.value)).toBeInTheDocument();
});
expect(getByText('View full log event message')).toBeInTheDocument();
});
it('should toggle slider - selected span details', async () => {
const { findByTestId, queryByText } = render(
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
<Route path={ROUTES.TRACE_DETAIL}>
<TraceDetail />
</Route>
,
</MemoryRouter>,
);
const slider = await findByTestId('span-details-sider');
expect(slider).toBeInTheDocument();
fireEvent.click(
slider.querySelector('.ant-layout-sider-trigger') as HTMLElement,
);
expect(queryByText('Details for selected Span')).not.toBeInTheDocument();
});
it('should be able to selected another span and see its detail', async () => {
const { getByText } = render(
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
<Route path={ROUTES.TRACE_DETAIL}>
<TraceDetail />
</Route>
,
</MemoryRouter>,
);
expect(await screen.findByText('Trace Details')).toBeInTheDocument();
const spanTitle = getByText('/driver.DriverService/FindNearest');
expect(spanTitle).toBeInTheDocument();
fireEvent.click(spanTitle);
// Tag details
[
{ title: 'client-uuid', value: '6fb81b8ca91b2b4d' },
{ title: 'component', value: 'gRPC' },
{ title: 'host.name', value: '4f6ec470feea' },
].forEach((tag) => {
expect(getByText(tag.title)).toBeInTheDocument();
expect(getByText(tag.value)).toBeInTheDocument();
});
});
it('focus on selected span and reset focus action', async () => {
const { getByText, getAllByText } = render(
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
<Route path={ROUTES.TRACE_DETAIL}>
<TraceDetail />
</Route>
,
</MemoryRouter>,
);
expect(await screen.findByText('Trace Details')).toBeInTheDocument();
const spanTitle = getByText('/driver.DriverService/FindNearest');
expect(spanTitle).toBeInTheDocument();
fireEvent.click(spanTitle);
expect(await screen.findByText('6fb81b8ca91b2b4d')).toBeInTheDocument();
// focus on selected span
const focusButton = getByText('Focus on selected span');
expect(focusButton).toBeInTheDocument();
fireEvent.click(focusButton);
// assert selected span
expect(getByText('15 Spans')).toBeInTheDocument();
expect(getAllByText('/driver.DriverService/FindNearest')).toHaveLength(3);
expect(getByText('173.10 ms')).toBeInTheDocument();
// reset focus
expect(screen.queryByText('HTTP GET /dispatch')).not.toBeInTheDocument();
const resetFocusButton = getByText('Reset Focus');
expect(resetFocusButton).toBeInTheDocument();
fireEvent.click(resetFocusButton);
expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled();
expect(screen.queryByText('HTTP GET /dispatch')).toBeInTheDocument();
});
});

View File

@@ -617,9 +617,7 @@ describe('TracesExplorer - ', () => {
const viewListOptions = await screen.findByRole('listbox');
expect(viewListOptions).toBeInTheDocument();
expect(
within(viewListOptions).getByText('success traces list view'),
).toBeInTheDocument();
expect(within(viewListOptions).getByText('R-test panel')).toBeInTheDocument();
expect(within(viewListOptions).getByText('Table View')).toBeInTheDocument();

View File

@@ -52,6 +52,8 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
ALL_CHANNELS: ['ADMIN', 'EDITOR', 'VIEWER'],
INGESTION_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
ALL_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'],
MESSAGING_QUEUES: ['ADMIN', 'EDITOR', 'VIEWER'],
MESSAGING_QUEUES_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'],
ALL_ERROR: ['ADMIN', 'EDITOR', 'VIEWER'],
APPLICATION: ['ADMIN', 'EDITOR', 'VIEWER'],
CHANNELS_EDIT: ['ADMIN'],
@@ -95,7 +97,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
API_KEYS: ['ADMIN'],
LOGS_BASE: [],
OLD_LOGS_EXPLORER: [],
OLD_LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],

2
go.mod
View File

@@ -6,7 +6,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.23.2
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.102.2
github.com/SigNoz/signoz-otel-collector v0.102.7
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
github.com/antonmedv/expr v1.15.3

4
go.sum
View File

@@ -64,8 +64,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg=
github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I=
github.com/SigNoz/signoz-otel-collector v0.102.2 h1:SmjsBZjMjTVVpuOlfJXlsDJQbdefQP/9Wz3CyzSuZuU=
github.com/SigNoz/signoz-otel-collector v0.102.2/go.mod h1:ISAXYhZenojCWg6CdDJtPMpfS6Zwc08+uoxH25tc6Y0=
github.com/SigNoz/signoz-otel-collector v0.102.7 h1:UBjO88GNCGZuWKl1LFukRahR1cu9AGwFHyObo07RrYA=
github.com/SigNoz/signoz-otel-collector v0.102.7/go.mod h1:3s9cSL8yexkBBMfK9mC3WWrAPm8oMtlZhvBxvt+Ziag=
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=

View File

@@ -805,7 +805,7 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request
continue
}
filterItems := []v3.FilterItem{}
if rule.AlertType == "LOGS_BASED_ALERT" || rule.AlertType == "TRACES_BASED_ALERT" {
if rule.AlertType == rules.AlertTypeLogs || rule.AlertType == rules.AlertTypeTraces {
if rule.RuleCondition.CompositeQuery != nil {
if rule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
for _, query := range rule.RuleCondition.CompositeQuery.BuilderQueries {
@@ -818,9 +818,9 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request
}
newFilters := common.PrepareFilters(lbls, filterItems)
ts := time.Unix(res.Items[idx].UnixMilli/1000, 0)
if rule.AlertType == "LOGS_BASED_ALERT" {
if rule.AlertType == rules.AlertTypeLogs {
res.Items[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, newFilters)
} else if rule.AlertType == "TRACES_BASED_ALERT" {
} else if rule.AlertType == rules.AlertTypeTraces {
res.Items[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, newFilters)
}
}
@@ -854,9 +854,9 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter,
}
ts := time.Unix(params.End/1000, 0)
filters := common.PrepareFilters(lbls, nil)
if rule.AlertType == "LOGS_BASED_ALERT" {
if rule.AlertType == rules.AlertTypeLogs {
res[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, filters)
} else if rule.AlertType == "TRACES_BASED_ALERT" {
} else if rule.AlertType == rules.AlertTypeTraces {
res[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, filters)
}
}
@@ -2496,10 +2496,113 @@ func (aH *APIHandler) RegisterMessagingQueuesRoutes(router *mux.Router, am *Auth
kafkaSubRouter.HandleFunc("/producer-details", am.ViewAccess(aH.getProducerData)).Methods(http.MethodPost)
kafkaSubRouter.HandleFunc("/consumer-details", am.ViewAccess(aH.getConsumerData)).Methods(http.MethodPost)
kafkaSubRouter.HandleFunc("/network-latency", am.ViewAccess(aH.getNetworkData)).Methods(http.MethodPost)
// for other messaging queues, add SubRouters here
}
// not using md5 hashing as the plain string would work
func uniqueIdentifier(clientID, serviceInstanceID, serviceName, separator string) string {
return clientID + separator + serviceInstanceID + separator + serviceName
}
func (aH *APIHandler) getNetworkData(
w http.ResponseWriter, r *http.Request,
) {
attributeCache := &mq.Clients{
Hash: make(map[string]struct{}),
}
messagingQueue, apiErr := ParseMessagingQueueBody(r)
if apiErr != nil {
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
queryRangeParams, err := mq.BuildQRParamsNetwork(messagingQueue, "throughput", attributeCache)
if err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
var result []*v3.Result
var errQueriesByName map[string]error
result, errQueriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams, nil)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQueriesByName)
return
}
for _, res := range result {
for _, series := range res.Series {
clientID, clientIDOk := series.Labels["client_id"]
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
serviceName, serviceNameOk := series.Labels["service_name"]
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
_, ok := attributeCache.Hash[hashKey]
if clientIDOk && serviceInstanceIDOk && serviceNameOk && !ok {
attributeCache.Hash[hashKey] = struct{}{}
attributeCache.ClientID = append(attributeCache.ClientID, clientID)
attributeCache.ServiceInstanceID = append(attributeCache.ServiceInstanceID, serviceInstanceID)
attributeCache.ServiceName = append(attributeCache.ServiceName, serviceName)
}
}
}
queryRangeParams, err = mq.BuildQRParamsNetwork(messagingQueue, "fetch-latency", attributeCache)
if err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
resultFetchLatency, errQueriesByNameFetchLatency, err := aH.querierV2.QueryRange(r.Context(), queryRangeParams, nil)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQueriesByNameFetchLatency)
return
}
latencyColumn := &v3.Result{QueryName: "latency"}
var latencySeries []*v3.Series
for _, res := range resultFetchLatency {
for _, series := range res.Series {
clientID, clientIDOk := series.Labels["client_id"]
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
serviceName, serviceNameOk := series.Labels["service_name"]
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
_, ok := attributeCache.Hash[hashKey]
if clientIDOk && serviceInstanceIDOk && serviceNameOk && ok {
latencySeries = append(latencySeries, series)
}
}
}
latencyColumn.Series = latencySeries
result = append(result, latencyColumn)
resultFetchLatency = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
resp := v3.QueryRangeResponse{
Result: resultFetchLatency,
}
aH.Respond(w, resp)
}
func (aH *APIHandler) getProducerData(
w http.ResponseWriter, r *http.Request,
) {

View File

@@ -1,11 +1,6 @@
## Consumer Lag feature break down
### 1) Consumer Lag Graph
---
### 2) Consumer Group Details
### 1) Consumer Group Details
API endpoint:
@@ -13,75 +8,75 @@ API endpoint:
POST /api/v1/messaging-queues/kafka/consumer-lag/consumer-details
```
Request-Body
```json
{
"start": 1720685296000000000,
"end": 1721290096000000000,
"variables": {
"partition": "0",
"topic": "topic1",
"consumer_group": "cg1"
}
"start": 1724429217000000000,
"end": 1724431017000000000,
"variables": {
"partition": "0",
"topic": "topic1",
"consumer_group": "cg1"
}
}
```
response in query range format `series`
Response in query range `table` format
```json
{
"status": "success",
"data": {
"resultType": "",
"result": [
"status": "success",
"data": {
"resultType": "",
"result": [
{
"table": {
"columns": [
{
"table": {
"columns": [
{
"name": "service_name",
"queryName": "",
"isValueColumn": false
},
{
"name": "p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "error_rate",
"queryName": "",
"isValueColumn": false
},
{
"name": "throughput",
"queryName": "",
"isValueColumn": false
},
{
"name": "avg_msg_size",
"queryName": "",
"isValueColumn": false
}
],
"rows": [
{
"data": {
"avg_msg_size": "0",
"error_rate": "0",
"p99": "0.2942205100000016",
"service_name": "consumer-svc",
"throughput": "0.00016534391534391533"
}
}
]
}
"name": "service_name",
"queryName": "",
"isValueColumn": false
},
{
"name": "p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "error_rate",
"queryName": "",
"isValueColumn": false
},
{
"name": "throughput",
"queryName": "",
"isValueColumn": false
},
{
"name": "avg_msg_size",
"queryName": "",
"isValueColumn": false
}
]
}
],
"rows": [
{
"data": {
"avg_msg_size": "15",
"error_rate": "0",
"p99": "0.47993265000000035",
"service_name": "consumer-svc",
"throughput": "39.86888888888889"
}
}
]
}
}
]
}
}
```
----
### 3) Producer Details
### 2) Producer Details
API endpoint:
@@ -89,18 +84,19 @@ API endpoint:
POST /api/v1/messaging-queues/kafka/consumer-lag/producer-details
```
Request-Body
```json
{
"start": 1720685296000000000,
"end": 1721290096000000000,
"start": 1724429217000000000,
"end": 1724431017000000000,
"variables": {
"partition": "0",
"partition": "0",
"topic": "topic1"
}
}
```
response in query range format `series`
Response in query range `table` format
```json
{
"status": "success",
@@ -116,17 +112,17 @@ response in query range format `series`
"isValueColumn": false
},
{
"name": "p99_query.p99",
"name": "p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "error_rate",
"name": "error_percentage",
"queryName": "",
"isValueColumn": false
},
{
"name": "rps",
"name": "throughput",
"queryName": "",
"isValueColumn": false
}
@@ -134,56 +130,9 @@ response in query range format `series`
"rows": [
{
"data": {
"error_rate": "0",
"p99_query.p99": "150.08830908000002",
"rps": "0.00016534391534391533",
"service_name": "producer-svc"
}
}
]
}
}
]
}
}
```
response in query range format `table`
```json
{
"status": "success",
"data": {
"resultType": "",
"result": [
{
"table": {
"columns": [
{
"name": "service_name",
"queryName": "",
"isValueColumn": false
},
{
"name": "p99_query.p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "error_rate",
"queryName": "",
"isValueColumn": false
},
{
"name": "rps",
"queryName": "",
"isValueColumn": false
}
],
"rows": [
{
"data": {
"error_rate": "0",
"p99_query.p99": "150.08830908000002",
"rps": "0.00016534391534391533",
"error_percentage": "0",
"p99": "5.51359028",
"throughput": "39.86888888888889",
"service_name": "producer-svc"
}
}
@@ -195,3 +144,85 @@ response in query range format `table`
}
```
### 3) Network Fetch Latency:
API endpoint:
```
POST /api/v1/messaging-queues/kafka/consumer-lag/network-latency
```
Request-Body
```json
{
"start": 1724673937000000000,
"end": 1724675737000000000,
"variables": {
"consumer_group": "cg1",
"partition": "0"
}
}
```
Response in query range `table` format
```json
{
"status": "success",
"data": {
"resultType": "",
"result": [
{
"table": {
"columns": [
{
"name": "service_name",
"queryName": "",
"isValueColumn": false
},
{
"name": "client_id",
"queryName": "",
"isValueColumn": false
},
{
"name": "service_instance_id",
"queryName": "",
"isValueColumn": false
},
{
"name": "latency",
"queryName": "latency",
"isValueColumn": true
},
{
"name": "throughput",
"queryName": "throughput",
"isValueColumn": true
}
],
"rows": [
{
"data": {
"client_id": "consumer-cg1-1",
"latency": 48.99,
"service_instance_id": "b0a851d7-1735-4e3f-8f5f-7c63a8a55a24",
"service_name": "consumer-svc",
"throughput": 14.97
}
},
{
"data": {
"client_id": "consumer-cg1-1",
"latency": 25.21,
"service_instance_id": "ccf49550-2e8f-4c7b-be29-b9e0891ef93d",
"service_name": "consumer-svc",
"throughput": 24.91
}
}
]
}
}
]
}
}
```

View File

@@ -7,3 +7,10 @@ type MessagingQueue struct {
End int64 `json:"end"`
Variables map[string]string `json:"variables,omitempty"`
}
type Clients struct {
Hash map[string]struct{}
ClientID []string
ServiceInstanceID []string
ServiceName []string
}

View File

@@ -26,7 +26,6 @@ WITH consumer_query AS (
GROUP BY serviceName
)
-- Main query to select all metrics
SELECT
serviceName AS service_name,
p99,
@@ -65,7 +64,7 @@ SELECT
serviceName AS service_name,
p99,
COALESCE((error_count * 100.0) / total_count, 0) AS error_percentage,
COALESCE(total_count / %d, 0) AS rps -- Convert nanoseconds to seconds
COALESCE(total_count / %d, 0) AS throughput -- Convert nanoseconds to seconds
FROM
producer_query
ORDER BY
@@ -74,3 +73,25 @@ ORDER BY
`, start, end, queueType, topic, partition, timeRange)
return query
}
func generateNetworkLatencyThroughputSQL(start, end int64, consumerGroup, partitionID, queueType string) string {
timeRange := (end - start) / 1000000000
query := fmt.Sprintf(`
SELECT
stringTagMap['messaging.client_id'] AS client_id,
stringTagMap['service.instance.id'] AS service_instance_id,
serviceName AS service_name,
count(*) / %d AS throughput
FROM signoz_traces.distributed_signoz_index_v2
WHERE
timestamp >= '%d'
AND timestamp <= '%d'
AND kind = 5
AND msgSystem = '%s'
AND stringTagMap['messaging.kafka.consumer.group'] = '%s'
AND stringTagMap['messaging.destination.partition.id'] = '%s'
GROUP BY service_name, client_id, service_instance_id
ORDER BY throughput DESC
`, timeRange, start, end, queueType, consumerGroup, partitionID)
return query
}

View File

@@ -2,7 +2,9 @@ package kafka
import (
"fmt"
"strings"
"go.signoz.io/signoz/pkg/query-service/common"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
)
@@ -35,6 +37,146 @@ func BuildQueryRangeParams(messagingQueue *MessagingQueue, queryContext string)
return queryRangeParams, nil
}
func buildClickHouseQueryNetwork(messagingQueue *MessagingQueue, queueType string) (*v3.ClickHouseQuery, error) {
start := messagingQueue.Start
end := messagingQueue.End
consumerGroup, ok := messagingQueue.Variables["consumer_group"]
if !ok {
return nil, fmt.Errorf("consumer_group not found in the request")
}
partitionID, ok := messagingQueue.Variables["partition"]
if !ok {
return nil, fmt.Errorf("partition not found in the request")
}
query := generateNetworkLatencyThroughputSQL(start, end, consumerGroup, partitionID, queueType)
return &v3.ClickHouseQuery{
Query: query,
}, nil
}
func formatstring(str []string) string {
joined := strings.Join(str, ", ")
if len(joined) <= 2 {
return ""
}
return joined[1 : len(joined)-1]
}
func buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd int64, attributeCache *Clients) (map[string]*v3.BuilderQuery, error) {
bq := make(map[string]*v3.BuilderQuery)
queryName := fmt.Sprintf("latency")
chq := &v3.BuilderQuery{
QueryName: queryName,
StepInterval: common.MinAllowedStepInterval(unixMilliStart, unixMilliEnd),
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: "kafka_consumer_fetch_latency_avg",
},
AggregateOperator: v3.AggregateOperatorAvg,
Temporality: v3.Unspecified,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationAvg,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "service_name",
Type: v3.AttributeKeyTypeTag,
DataType: v3.AttributeKeyDataTypeString,
},
Operator: v3.FilterOperatorIn,
Value: attributeCache.ServiceName,
},
{
Key: v3.AttributeKey{
Key: "client_id",
Type: v3.AttributeKeyTypeTag,
DataType: v3.AttributeKeyDataTypeString,
},
Operator: v3.FilterOperatorIn,
Value: attributeCache.ClientID,
},
{
Key: v3.AttributeKey{
Key: "service_instance_id",
Type: v3.AttributeKeyTypeTag,
DataType: v3.AttributeKeyDataTypeString,
},
Operator: v3.FilterOperatorIn,
Value: attributeCache.ServiceInstanceID,
},
},
},
Expression: queryName,
ReduceTo: v3.ReduceToOperatorAvg,
GroupBy: []v3.AttributeKey{{
Key: "service_name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
{
Key: "client_id",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
{
Key: "service_instance_id",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
},
}
bq[queryName] = chq
return bq, nil
}
func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, attributeCache *Clients) (*v3.QueryRangeParamsV3, error) {
queueType := kafkaQueue
unixMilliStart := messagingQueue.Start / 1000000
unixMilliEnd := messagingQueue.End / 1000000
var cq *v3.CompositeQuery
if queryContext == "throughput" {
chq, err := buildClickHouseQueryNetwork(messagingQueue, queueType)
if err != nil {
return nil, err
}
cq, err = buildCompositeQuery(chq, queryContext)
} else if queryContext == "fetch-latency" {
bhq, err := buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd, attributeCache)
if err != nil {
return nil, err
}
cq = &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
BuilderQueries: bhq,
PanelType: v3.PanelTypeTable,
}
}
queryRangeParams := &v3.QueryRangeParamsV3{
Start: unixMilliStart,
End: unixMilliEnd,
Step: defaultStepInterval,
CompositeQuery: cq,
Version: "v4",
FormatForWeb: true,
}
return queryRangeParams, nil
}
func buildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, queryContext string) (*v3.ClickHouseQuery, error) {
start := messagingQueue.Start
end := messagingQueue.End
@@ -48,15 +190,14 @@ func buildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer
return nil, fmt.Errorf("invalid type for Partition")
}
consumerGroup, ok := messagingQueue.Variables["consumer_group"]
if !ok {
return nil, fmt.Errorf("invalid type for consumer group")
}
var query string
if queryContext == "producer" {
query = generateProducerSQL(start, end, topic, partition, queueType)
} else if queryContext == "consumer" {
consumerGroup, ok := messagingQueue.Variables["consumer_group"]
if !ok {
return nil, fmt.Errorf("invalid type for consumer group")
}
query = generateConsumerSQL(start, end, topic, partition, consumerGroup, queueType)
}

View File

@@ -126,7 +126,7 @@ func PrepareLinksToTraces(ts time.Time, filterItems []v3.FilterItem) string {
}
data, _ := json.Marshal(urlData)
compositeQuery := url.QueryEscape(string(data))
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
optionsData, _ := json.Marshal(options)
urlEncodedOptions := url.QueryEscape(string(optionsData))
@@ -185,7 +185,7 @@ func PrepareLinksToLogs(ts time.Time, filterItems []v3.FilterItem) string {
}
data, _ := json.Marshal(urlData)
compositeQuery := url.QueryEscape(string(data))
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
optionsData, _ := json.Marshal(options)
urlEncodedOptions := url.QueryEscape(string(optionsData))

View File

@@ -1,153 +0,0 @@
package alertstov4
import (
"context"
"encoding/json"
"github.com/jmoiron/sqlx"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/rules"
"go.uber.org/multierr"
"go.uber.org/zap"
)
var Version = "0.45-alerts-to-v4"
var mapTimeAggregation = map[v3.AggregateOperator]v3.TimeAggregation{
v3.AggregateOperatorSum: v3.TimeAggregationSum,
v3.AggregateOperatorMin: v3.TimeAggregationMin,
v3.AggregateOperatorMax: v3.TimeAggregationMax,
v3.AggregateOperatorSumRate: v3.TimeAggregationRate,
v3.AggregateOperatorAvgRate: v3.TimeAggregationRate,
v3.AggregateOperatorMinRate: v3.TimeAggregationRate,
v3.AggregateOperatorMaxRate: v3.TimeAggregationRate,
v3.AggregateOperatorHistQuant50: v3.TimeAggregationUnspecified,
v3.AggregateOperatorHistQuant75: v3.TimeAggregationUnspecified,
v3.AggregateOperatorHistQuant90: v3.TimeAggregationUnspecified,
v3.AggregateOperatorHistQuant95: v3.TimeAggregationUnspecified,
v3.AggregateOperatorHistQuant99: v3.TimeAggregationUnspecified,
}
var mapSpaceAggregation = map[v3.AggregateOperator]v3.SpaceAggregation{
v3.AggregateOperatorSum: v3.SpaceAggregationSum,
v3.AggregateOperatorMin: v3.SpaceAggregationMin,
v3.AggregateOperatorMax: v3.SpaceAggregationMax,
v3.AggregateOperatorSumRate: v3.SpaceAggregationSum,
v3.AggregateOperatorAvgRate: v3.SpaceAggregationAvg,
v3.AggregateOperatorMinRate: v3.SpaceAggregationMin,
v3.AggregateOperatorMaxRate: v3.SpaceAggregationMax,
v3.AggregateOperatorHistQuant50: v3.SpaceAggregationPercentile50,
v3.AggregateOperatorHistQuant75: v3.SpaceAggregationPercentile75,
v3.AggregateOperatorHistQuant90: v3.SpaceAggregationPercentile90,
v3.AggregateOperatorHistQuant95: v3.SpaceAggregationPercentile95,
v3.AggregateOperatorHistQuant99: v3.SpaceAggregationPercentile99,
}
func canMigrateOperator(operator v3.AggregateOperator) bool {
switch operator {
case v3.AggregateOperatorSum,
v3.AggregateOperatorMin,
v3.AggregateOperatorMax,
v3.AggregateOperatorSumRate,
v3.AggregateOperatorAvgRate,
v3.AggregateOperatorMinRate,
v3.AggregateOperatorMaxRate,
v3.AggregateOperatorHistQuant50,
v3.AggregateOperatorHistQuant75,
v3.AggregateOperatorHistQuant90,
v3.AggregateOperatorHistQuant95,
v3.AggregateOperatorHistQuant99:
return true
}
return false
}
func Migrate(conn *sqlx.DB) error {
ruleDB := rules.NewRuleDB(conn)
storedRules, err := ruleDB.GetStoredRules(context.Background())
if err != nil {
return err
}
for _, storedRule := range storedRules {
parsedRule, errs := rules.ParsePostableRule([]byte(storedRule.Data))
if len(errs) > 0 {
// this should not happen but if it does, we should not stop the migration
zap.L().Error("Error parsing rule", zap.Error(multierr.Combine(errs...)), zap.Int("rule", storedRule.Id))
continue
}
zap.L().Info("Rule parsed", zap.Int("rule", storedRule.Id))
updated := false
if parsedRule.RuleCondition != nil && parsedRule.Version == "" {
if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
// check if all the queries can be converted to v4
canMigrate := true
for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries {
if query.DataSource == v3.DataSourceMetrics && query.Expression == query.QueryName {
if !canMigrateOperator(query.AggregateOperator) {
canMigrate = false
break
}
}
}
if canMigrate {
parsedRule.Version = "v4"
for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries {
if query.DataSource == v3.DataSourceMetrics && query.Expression == query.QueryName {
// update aggregate attribute
if query.AggregateOperator == v3.AggregateOperatorSum ||
query.AggregateOperator == v3.AggregateOperatorMin ||
query.AggregateOperator == v3.AggregateOperatorMax {
query.AggregateAttribute.Type = "Gauge"
}
if query.AggregateOperator == v3.AggregateOperatorSumRate ||
query.AggregateOperator == v3.AggregateOperatorAvgRate ||
query.AggregateOperator == v3.AggregateOperatorMinRate ||
query.AggregateOperator == v3.AggregateOperatorMaxRate {
query.AggregateAttribute.Type = "Sum"
}
if query.AggregateOperator == v3.AggregateOperatorHistQuant50 ||
query.AggregateOperator == v3.AggregateOperatorHistQuant75 ||
query.AggregateOperator == v3.AggregateOperatorHistQuant90 ||
query.AggregateOperator == v3.AggregateOperatorHistQuant95 ||
query.AggregateOperator == v3.AggregateOperatorHistQuant99 {
query.AggregateAttribute.Type = "Histogram"
}
query.AggregateAttribute.DataType = v3.AttributeKeyDataTypeFloat64
query.AggregateAttribute.IsColumn = true
query.TimeAggregation = mapTimeAggregation[query.AggregateOperator]
query.SpaceAggregation = mapSpaceAggregation[query.AggregateOperator]
query.AggregateOperator = v3.AggregateOperator(query.TimeAggregation)
updated = true
}
}
}
}
}
if !updated {
zap.L().Info("Rule not updated", zap.Int("rule", storedRule.Id))
continue
}
ruleJSON, jsonErr := json.Marshal(parsedRule)
if jsonErr != nil {
zap.L().Error("Error marshalling rule; skipping rule migration", zap.Error(jsonErr), zap.Int("rule", storedRule.Id))
continue
}
stmt, prepareError := conn.PrepareContext(context.Background(), `UPDATE rules SET data=$3 WHERE id=$4;`)
if prepareError != nil {
zap.L().Error("Error in preparing statement for UPDATE to rules", zap.Error(prepareError))
continue
}
defer stmt.Close()
if _, err := stmt.Exec(ruleJSON, storedRule.Id); err != nil {
zap.L().Error("Error in Executing prepared statement for UPDATE to rules", zap.Error(err))
}
}
return nil
}

View File

@@ -1,70 +0,0 @@
package alertscustomstep
import (
"context"
"encoding/json"
"time"
"github.com/jmoiron/sqlx"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/rules"
"go.uber.org/multierr"
"go.uber.org/zap"
)
var Version = "0.47-alerts-custom-step"
func Migrate(conn *sqlx.DB) error {
ruleDB := rules.NewRuleDB(conn)
storedRules, err := ruleDB.GetStoredRules(context.Background())
if err != nil {
return err
}
for _, storedRule := range storedRules {
parsedRule, errs := rules.ParsePostableRule([]byte(storedRule.Data))
if len(errs) > 0 {
// this should not happen but if it does, we should not stop the migration
zap.L().Error("Error parsing rule", zap.Error(multierr.Combine(errs...)), zap.Int("rule", storedRule.Id))
continue
}
zap.L().Info("Rule parsed", zap.Int("rule", storedRule.Id))
updated := false
if parsedRule.RuleCondition != nil {
if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
if parsedRule.EvalWindow <= rules.Duration(6*time.Hour) {
for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries {
if query.StepInterval > 60 {
updated = true
zap.L().Info("Updating step interval", zap.Int("rule", storedRule.Id), zap.Int64("old", query.StepInterval), zap.Int64("new", 60))
query.StepInterval = 60
}
}
}
}
}
if !updated {
zap.L().Info("Rule not updated", zap.Int("rule", storedRule.Id))
continue
}
ruleJSON, jsonErr := json.Marshal(parsedRule)
if jsonErr != nil {
zap.L().Error("Error marshalling rule; skipping rule migration", zap.Error(jsonErr), zap.Int("rule", storedRule.Id))
continue
}
stmt, prepareError := conn.PrepareContext(context.Background(), `UPDATE rules SET data=$3 WHERE id=$4;`)
if prepareError != nil {
zap.L().Error("Error in preparing statement for UPDATE to rules", zap.Error(prepareError))
continue
}
defer stmt.Close()
if _, err := stmt.Exec(ruleJSON, storedRule.Id); err != nil {
zap.L().Error("Error in Executing prepared statement for UPDATE to rules", zap.Error(err))
}
}
return nil
}

View File

@@ -7,9 +7,6 @@ import (
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/jmoiron/sqlx"
alertstov4 "go.signoz.io/signoz/pkg/query-service/migrate/0_45_alerts_to_v4"
alertscustomstep "go.signoz.io/signoz/pkg/query-service/migrate/0_47_alerts_custom_step"
"go.uber.org/zap"
)
type DataMigration struct {
@@ -56,28 +53,6 @@ func Migrate(dsn string) error {
return err
}
if m, err := getMigrationVersion(conn, "0.45_alerts_to_v4"); err == nil && m == nil {
if err := alertstov4.Migrate(conn); err != nil {
zap.L().Error("failed to migrate 0.45_alerts_to_v4", zap.Error(err))
} else {
_, err := conn.Exec("INSERT INTO data_migrations (version, succeeded) VALUES ('0.45_alerts_to_v4', true)")
if err != nil {
return err
}
}
}
if m, err := getMigrationVersion(conn, "0.47_alerts_custom_step"); err == nil && m == nil {
if err := alertscustomstep.Migrate(conn); err != nil {
zap.L().Error("failed to migrate 0.47_alerts_custom_step", zap.Error(err))
} else {
_, err := conn.Exec("INSERT INTO data_migrations (version, succeeded) VALUES ('0.47_alerts_custom_step', true)")
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -61,6 +61,35 @@ func (s AlertState) String() string {
panic(errors.Errorf("unknown alert state: %d", s))
}
func (s AlertState) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
func (s *AlertState) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case string:
switch value {
case "inactive":
*s = StateInactive
case "pending":
*s = StatePending
case "firing":
*s = StateFiring
case "disabled":
*s = StateDisabled
default:
return errors.New("invalid alert state")
}
return nil
default:
return errors.New("invalid alert state")
}
}
type Alert struct {
State AlertState

View File

@@ -16,6 +16,22 @@ import (
yaml "gopkg.in/yaml.v2"
)
type AlertType string
const (
AlertTypeMetric AlertType = "METRIC_BASED_ALERT"
AlertTypeTraces AlertType = "TRACES_BASED_ALERT"
AlertTypeLogs AlertType = "LOGS_BASED_ALERT"
AlertTypeExceptions AlertType = "EXCEPTIONS_BASED_ALERT"
)
type RuleDataKind string
const (
RuleDataKindJson RuleDataKind = "json"
RuleDataKindYaml RuleDataKind = "yaml"
)
// this file contains api request and responses to be
// served over http
@@ -31,12 +47,12 @@ func newApiErrorBadData(err error) *model.ApiError {
// PostableRule is used to create alerting rule from HTTP api
type PostableRule struct {
AlertName string `yaml:"alert,omitempty" json:"alert,omitempty"`
AlertType string `yaml:"alertType,omitempty" json:"alertType,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"`
EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"`
Frequency Duration `yaml:"frequency,omitempty" json:"frequency,omitempty"`
AlertName string `yaml:"alert,omitempty" json:"alert,omitempty"`
AlertType AlertType `yaml:"alertType,omitempty" json:"alertType,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"`
EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"`
Frequency Duration `yaml:"frequency,omitempty" json:"frequency,omitempty"`
RuleCondition *RuleCondition `yaml:"condition,omitempty" json:"condition,omitempty"`
Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
@@ -234,8 +250,8 @@ type GettableRules struct {
// GettableRule has info for an alerting rules.
type GettableRule struct {
Id string `json:"id"`
State string `json:"state"`
Id string `json:"id"`
State AlertState `json:"state"`
PostableRule
CreatedAt *time.Time `json:"createAt"`
CreatedBy *string `json:"createBy"`

View File

@@ -325,9 +325,9 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
continue
}
alertNames = append(alertNames, rule.AlertName)
if rule.AlertType == "LOGS_BASED_ALERT" {
if rule.AlertType == AlertTypeLogs {
alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1
} else if rule.AlertType == "METRIC_BASED_ALERT" {
} else if rule.AlertType == AlertTypeMetric {
alertsInfo.MetricBasedAlerts = alertsInfo.MetricBasedAlerts + 1
if rule.RuleCondition != nil && rule.RuleCondition.CompositeQuery != nil {
if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeBuilder {
@@ -343,7 +343,7 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
}
}
}
} else if rule.AlertType == "TRACES_BASED_ALERT" {
} else if rule.AlertType == AlertTypeTraces {
alertsInfo.TracesBasedAlerts = alertsInfo.TracesBasedAlerts + 1
}
alertsInfo.TotalAlerts = alertsInfo.TotalAlerts + 1

View File

@@ -20,11 +20,9 @@ import (
"github.com/jmoiron/sqlx"
// opentracing "github.com/opentracing/opentracing-go"
am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
"go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/query-service/utils/labels"
)
@@ -240,20 +238,6 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error
parsedRule, errs := ParsePostableRule([]byte(ruleStr))
currentRule, err := m.GetRule(ctx, id)
if err != nil {
zap.L().Error("failed to get the rule from rule db", zap.String("id", id), zap.Error(err))
return err
}
if !checkIfTraceOrLogQB(&currentRule.PostableRule) {
// check if the new rule uses any feature that is not enabled
err = m.checkFeatureUsage(parsedRule)
if err != nil {
return err
}
}
if len(errs) > 0 {
zap.L().Error("failed to parse rules", zap.Errors("errors", errs))
// just one rule is being parsed so expect just one error
@@ -272,20 +256,6 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error
}
}
// update feature usage if the current rule is not a trace or log query builder
if !checkIfTraceOrLogQB(&currentRule.PostableRule) {
err = m.updateFeatureUsage(parsedRule, 1)
if err != nil {
zap.L().Error("error updating feature usage", zap.Error(err))
}
// update feature usage if the new rule is not a trace or log query builder and the current rule is
} else if !checkIfTraceOrLogQB(parsedRule) {
err = m.updateFeatureUsage(&currentRule.PostableRule, -1)
if err != nil {
zap.L().Error("error updating feature usage", zap.Error(err))
}
}
return nil
}
@@ -335,13 +305,6 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error {
return fmt.Errorf("delete rule received an rule id in invalid format, must be a number")
}
// update feature usage
rule, err := m.GetRule(ctx, id)
if err != nil {
zap.L().Error("failed to get the rule from rule db", zap.String("id", id), zap.Error(err))
return err
}
taskName := prepareTaskName(int64(idInt))
if !m.opts.DisableRules {
m.deleteTask(taskName)
@@ -352,11 +315,6 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error {
return err
}
err = m.updateFeatureUsage(&rule.PostableRule, -1)
if err != nil {
zap.L().Error("error updating feature usage", zap.Error(err))
}
return nil
}
@@ -381,12 +339,6 @@ func (m *Manager) deleteTask(taskName string) {
func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule, error) {
parsedRule, errs := ParsePostableRule([]byte(ruleStr))
// check if the rule uses any feature that is not enabled
err := m.checkFeatureUsage(parsedRule)
if err != nil {
return nil, err
}
if len(errs) > 0 {
zap.L().Error("failed to parse rules", zap.Errors("errors", errs))
// just one rule is being parsed so expect just one error
@@ -409,11 +361,6 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule
return nil, err
}
// update feature usage
err = m.updateFeatureUsage(parsedRule, 1)
if err != nil {
zap.L().Error("error updating feature usage", zap.Error(err))
}
gettableRule := &GettableRule{
Id: fmt.Sprintf("%d", lastInsertId),
PostableRule: *parsedRule,
@@ -421,59 +368,6 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule
return gettableRule, nil
}
func (m *Manager) updateFeatureUsage(parsedRule *PostableRule, usage int64) error {
isTraceOrLogQB := checkIfTraceOrLogQB(parsedRule)
if isTraceOrLogQB {
feature, err := m.featureFlags.GetFeatureFlag(model.QueryBuilderAlerts)
if err != nil {
return err
}
feature.Usage += usage
if feature.Usage == feature.UsageLimit && feature.UsageLimit != -1 {
feature.Active = false
}
if feature.Usage < feature.UsageLimit || feature.UsageLimit == -1 {
feature.Active = true
}
err = m.featureFlags.UpdateFeatureFlag(feature)
if err != nil {
return err
}
}
return nil
}
func (m *Manager) checkFeatureUsage(parsedRule *PostableRule) error {
isTraceOrLogQB := checkIfTraceOrLogQB(parsedRule)
if isTraceOrLogQB {
err := m.featureFlags.CheckFeature(model.QueryBuilderAlerts)
if err != nil {
switch err.(type) {
case model.ErrFeatureUnavailable:
zap.L().Error("feature unavailable", zap.String("featureKey", model.QueryBuilderAlerts), zap.Error(err))
return model.BadRequest(err)
default:
zap.L().Error("feature check failed", zap.String("featureKey", model.QueryBuilderAlerts), zap.Error(err))
return model.BadRequest(err)
}
}
}
return nil
}
func checkIfTraceOrLogQB(parsedRule *PostableRule) bool {
if parsedRule != nil {
if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries {
if query.DataSource == v3.DataSourceTraces || query.DataSource == v3.DataSourceLogs {
return true
}
}
}
}
return false
}
func (m *Manager) addTask(rule *PostableRule, taskName string) error {
m.mtx.Lock()
defer m.mtx.Unlock()
@@ -569,7 +463,7 @@ func (m *Manager) prepareTask(acquireLock bool, r *PostableRule, taskName string
m.rules[ruleId] = pr
} else {
return nil, fmt.Errorf(fmt.Sprintf("unsupported rule type. Supported types: %s, %s", RuleTypeProm, RuleTypeThreshold))
return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", RuleTypeProm, RuleTypeThreshold)
}
return task, nil
@@ -710,10 +604,10 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*GettableRules, error) {
// fetch state of rule from memory
if rm, ok := m.rules[ruleResponse.Id]; !ok {
ruleResponse.State = StateDisabled.String()
ruleResponse.State = StateDisabled
ruleResponse.Disabled = true
} else {
ruleResponse.State = rm.State().String()
ruleResponse.State = rm.State()
}
ruleResponse.CreatedAt = s.CreatedAt
ruleResponse.CreatedBy = s.CreatedBy
@@ -737,10 +631,10 @@ func (m *Manager) GetRule(ctx context.Context, id string) (*GettableRule, error)
r.Id = fmt.Sprintf("%d", s.Id)
// fetch state of rule from memory
if rm, ok := m.rules[r.Id]; !ok {
r.State = StateDisabled.String()
r.State = StateDisabled
r.Disabled = true
} else {
r.State = rm.State().String()
r.State = rm.State()
}
r.CreatedAt = s.CreatedAt
r.CreatedBy = s.CreatedBy
@@ -846,10 +740,10 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleId string)
// fetch state of rule from memory
if rm, ok := m.rules[ruleId]; !ok {
response.State = StateDisabled.String()
response.State = StateDisabled
response.Disabled = true
} else {
response.State = rm.State().String()
response.State = rm.State()
}
return &response, nil

View File

@@ -91,8 +91,7 @@ type ThresholdRule struct {
lastTimestampWithDatapoints time.Time
// Type of the rule
// One of ["LOGS_BASED_ALERT", "TRACES_BASED_ALERT", "METRIC_BASED_ALERT", "EXCEPTIONS_BASED_ALERT"]
typ string
typ AlertType
// querier is used for alerts created before the introduction of new metrics query builder
querier interfaces.Querier
@@ -671,7 +670,7 @@ func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) str
}
data, _ := json.Marshal(urlData)
compositeQuery := url.QueryEscape(string(data))
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
optionsData, _ := json.Marshal(options)
urlEncodedOptions := url.QueryEscape(string(optionsData))
@@ -735,7 +734,7 @@ func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) s
}
data, _ := json.Marshal(urlData)
compositeQuery := url.QueryEscape(string(data))
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
optionsData, _ := json.Marshal(options)
urlEncodedOptions := url.QueryEscape(string(optionsData))
@@ -975,12 +974,12 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie
// Links with timestamps should go in annotations since labels
// is used alert grouping, and we want to group alerts with the same
// label set, but different timestamps, together.
if r.typ == "TRACES_BASED_ALERT" {
if r.typ == AlertTypeTraces {
link := r.prepareLinksToTraces(ts, smpl.MetricOrig)
if link != "" && r.hostFromSource() != "" {
annotations = append(annotations, labels.Label{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)})
}
} else if r.typ == "LOGS_BASED_ALERT" {
} else if r.typ == AlertTypeLogs {
link := r.prepareLinksToLogs(ts, smpl.MetricOrig)
if link != "" && r.hostFromSource() != "" {
annotations = append(annotations, labels.Label{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)})

View File

@@ -674,7 +674,7 @@ func TestNormalizeLabelName(t *testing.T) {
func TestPrepareLinksToLogs(t *testing.T) {
postableRule := PostableRule{
AlertName: "Tricky Condition Tests",
AlertType: "LOGS_BASED_ALERT",
AlertType: AlertTypeLogs,
RuleType: RuleTypeThreshold,
EvalWindow: Duration(5 * time.Minute),
Frequency: Duration(1 * time.Minute),

View File

@@ -649,12 +649,12 @@
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
-->
<!--
<macros>
<shard>01</shard>
<replica>example01-01-1</replica>
</macros>
-->
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->

View File

@@ -192,7 +192,7 @@ services:
<<: *db-depend
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -205,7 +205,7 @@ services:
# condition: service_healthy
otel-collector:
image: signoz/signoz-otel-collector:0.102.2
image: signoz/signoz-otel-collector:0.102.7
container_name: signoz-otel-collector
command:
[

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