Compare commits

...

48 Commits

Author SHA1 Message Date
Raj Kamal Singh
1411ae41c3 Revert "Feat: use new logspipelineprocessor for generating logs pipeline coll…" (#6099)
This reverts commit e4d1452f5f.
2024-09-30 23:46:34 +05:30
Prashant Shahi
bc8891d2f8 Sync/post release v0.55 (#6092) 2024-09-30 18:02:56 +05:30
Yunus M
3b7455ac4c fix: wait for licenseData to resolve before the check to show chat support (#6091) 2024-09-30 12:41:11 +05:30
SagarRajput-7
5a0a7c2c60 fix: restricted column long value to 3 line and line clamped (#6056)
* fix: restricted column long value to 3 line and line clamped

* fix: added tooltip prop as prop for generic component and passed style from consumer

* fix: comment resolved

* fix: refactored styles

* fix: updated snapshot

* fix: removed console log
2024-09-30 11:48:48 +05:30
rahulkeswani101
794d6fc0ca feat: added a new logo for sidebar and login page (#6005)
* feat: added a new logo for sidebar and login page

* chore: removed the signoz-signup.svg

* feat: updated the svg

* feat: updated the svg
2024-09-30 11:30:41 +05:30
Vishal Sharma
4c95df44d5 chore: update trial end workspace event (#6088) 2024-09-27 18:20:17 +05:30
Yunus M
717545e14c feat: remove sidebar dock option (#6083)
* feat: remove sidebar doc option

* feat: remove all references of sidebar collapse
2024-09-27 12:08:25 +05:30
Raj Kamal Singh
e4d1452f5f Feat: use new logspipelineprocessor for generating logs pipeline collector conf (#6080)
* chore: update logs pipeline prefix for generated collector config

* chore: some cleanup

* chore: some more cleanup

* chore: some more cleanup
2024-09-27 10:40:02 +05:30
rahulkeswani101
88ace79a64 feat: added meta tag to prevent page indexing (#5793)
* feat: added meta tag to prevent page indexing

* chore: revert to previous version

---------

Co-authored-by: Pranay Prateek <pranay@signoz.io>
2024-09-27 00:22:42 +05:30
Sudeep MP
9b42326f80 chore(trial end): analytics events added (#6048) 2024-09-26 23:30:58 +05:30
Sudeep MP
44a3469b9b style: enhance FAQ container styling and adjust layout for customer s… (#5999)
* style: enhance FAQ container styling and adjust layout for customer stories

fixed the button casing
faq layout shift issue due to parent flex centered

* style: add dark theme support to WorkspaceLocked styles

* refactor: moved inline styles to class
2024-09-26 20:44:34 +05:30
Vikrant Gupta
ef4b70f67b fix: intermittent undefined page in trace details page (#6084) 2024-09-26 19:06:22 +05:30
Vishal Sharma
7a125e31ec chore: remove slack connect and call help (#6044)
* chore: remove slack connect and call help
2024-09-26 17:33:21 +05:30
rahulkeswani101
6e3141a4ce feat: added blur event to having input in query section (#5684)
* feat: added blur event to having input in query section

* feat: added a error message for incomplete having clause and improved handleBlur

* feat: added focus event to remove error message

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-09-26 16:12:49 +05:30
Raj Kamal Singh
fc8391c5aa Feat: logs filter suggestions higher rank for special resource attribs like service.name and env etc (#6060)
* chore: upgrade signoz-otel-collector dependency to v0.102.10

* feat: first stab at ranking resource attribs higher

* chore: add test todo for validating resource attribs get ranked higher in logs filter suggestions

* chore: add test validating higher ranking for special resource attribs

* chore: some cleanup

* chore: some more cleanup
2024-09-26 15:26:14 +05:30
Vikrant Gupta
55f653d92e fix: added support for body contains X tag on pressing enter after selecting attribute key (#6059)
* fix: added empty operator in the top to support body contains

* fix: address review comments
2024-09-25 20:31:06 +05:30
SagarRajput-7
35f8e133a9 fix: dashboard variable - ux and usability fixes (#6038)
* fix: dashboard variable - ux and usability fixes

* fix: separarted all option, fixed tooltip handling, added clear option etc
2024-09-25 11:32:19 +05:30
SagarRajput-7
58d6487f77 fix: fixed extra space at bottom for list and table panel (#6049) 2024-09-25 11:32:06 +05:30
Raj Kamal Singh
708158f50f chore: move clickhousereader filter suggestion methods to dedicated file (#6061) 2024-09-25 09:51:16 +05:30
Kobe Cai
0feab5aa93 fix: check alert rule queries are all disabled if at least one query is set (#5966) 2024-09-24 11:20:45 +05:30
Srikanth Chekuri
b49ed913c7 chore: handle error before using task (#6055) 2024-09-24 10:56:49 +05:30
Srikanth Chekuri
419d2da363 feat: add anomaly rule (#5973) 2024-09-24 10:22:52 +05:30
Vikrant Gupta
df2844ea74 fix: the tag key flickering when moving from traces to logs (#6054) 2024-09-24 09:08:24 +05:30
Nityananda Gohain
5e5f0f167f TTL API for logs V2 (#5926)
* feat: ttl api for logs

* fix: add comments

* fix: add materialize_ttl_after_modify

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-09-23 20:12:38 +05:30
Vikrant Gupta
a6b05f0a3d fix: light mode design for qb v2 (#6052) 2024-09-23 15:50:46 +05:30
Nityananda Gohain
f69aaa2cfb fix: telemetry nil pointer error fix (#6051) 2024-09-23 13:15:21 +05:30
Nityananda Gohain
3866f89d3e feat: support for case insensitive for contains and like queries (#6045)
* feat: support for case insensitive for contains and like queries

* fix: make index filter lowercase for like and not like
2024-09-23 12:27:14 +05:30
Vikrant Gupta
f9ac41b865 feat: enable the search v2 for logs explorer page and remove FF (#5952) 2024-09-23 09:34:59 +05:30
rahulkeswani101
c5b5bfe540 feat: added new tab for infra metrics in logs detailed page (#5771)
* feat: added new tab for infra metrics in logs detailed page

* feat: added yaxis unit for the charts

* chore: cleanup query_range params

* fix: clusterName, podName variables not working

* feat: added skeleton for each charts in infra metrics tab

* change card height to 300px

* fix: updated the test cases

* feat: added new sub-tabs node and pod for infra metrics tab

* feat: added new components for node and pod metrics

* feat: added card titles for host metrics and handled empty state

* fix: updated the constant for host name

* feat: added vertical dotted line to all panels and updated y axis units for all panels

* feat: removed other panel types other than graph from host metrics query payload

* fix: updated the query payload for node metrics

* feat: moved the label of vertical dotted line to top

* feat: added console statement to check query payload

* fix: added pod name instead of node name in pod query payload

* fix: added key as pod name instead of node name in file system usage

* fix: updated query payload for file system usage in pod metrics and removed label from dotted line

* fix: updated the y axis units for network io

* fix: custom date time issue while plotting the graph

* feat: compare end time and current time update the end time accordingly

* feat: added the start and end time in query payloads

* refactor: removed the comments and unused variables

* chore: added a todo to make common component for sub-tabs

* fix: addressed review comments

---------

Co-authored-by: Ankit Nayan <ankit@signoz.io>
2024-09-20 23:56:34 +05:30
Vikrant Gupta
f3c01a5155 fix: export as csv for logs and traces table panel type (#6047)
* fix: export as csv for logs and traces panel type

* chore: remove console logs
2024-09-20 23:37:15 +05:30
Srikanth Chekuri
033b64a62a chore: add support for caching multiple time ranges for cache key (#6008) 2024-09-20 19:23:01 +05:30
Vikrant Gupta
4aabfe7cf5 fix: invalidate the cache for the alerts rules post update call (#6046) 2024-09-20 18:13:55 +05:30
Vikrant Gupta
0218f701b2 fix: alerts links are broken when there is a space in value (#6043)
* fix: space between values being converted as + sign in alerts generated links

* fix: added inline comment

* fix: added inline comment
2024-09-20 18:12:16 +05:30
Nityananda Gohain
f6d3f95768 fix: tlemetry for dashboard/alerts/views using contains on attributes (#6034)
* fix: tlemetry for dashboard/alerts/views using contains on attributes

* fix: update how telemetry is collected for logs

* fix: revert constands

* fix: check assertion for operator
2024-09-20 18:02:33 +05:30
SagarRajput-7
cb1cd3555b feat: added global search on table panel (#5893)
* feat: added global search on table panel

* feat: added global search on table panel

* feat: added global search conditionally and with new design

* feat: removed state from datasource

* feat: added global search in full view

* feat: added lightMode styles

* feat: added test cases for querytable and widgetHeader - global search
2024-09-20 16:36:35 +05:30
Vishal Sharma
ced72f86a4 doc: add info on request dashboard to contributing md (#6040) 2024-09-20 13:27:18 +05:30
SagarRajput-7
54d5666b92 fix: fixed dashboard header and list title alignment (#6035)
* fix: fixed dashboard header and list title alignment

* fix: fixed dashboard header and list title alignment

* fix: fixed existing styles
2024-09-20 11:39:10 +05:30
Srikanth Chekuri
4edc6dbeae feat: add last option to alert condition match type (#5929) 2024-09-19 23:21:31 +05:30
Vikrant Gupta
e203276678 chore: improve colors for the log line indicators (#6032) 2024-09-19 23:02:32 +05:30
Nityananda Gohain
8eb2cf144e fix: issues with like and ilike fixed in v4 qb (#6018) 2024-09-19 21:20:57 +05:30
Vikrant Gupta
2f7d208eb5 fix: added time range key for query and local storage handling (#6009)
* fix: added time range key for query and local storage handling

* chore: fix jest test cases

* fix: send single element array for only variable option as well

* fix: intermediate stale data should not be shown
2024-09-19 19:23:12 +05:30
SagarRajput-7
70fb5af19f chore: removed empty signoz-core-ui folder (#6030) 2024-09-19 18:18:37 +04:30
Vishal Sharma
fc7a94fa66 chore: update dashboard contributing doc and issue template (#6029)
* chore: update dashboard contributing doc and issue template

* chore: update issue template
2024-09-19 18:48:37 +05:30
Vishal Sharma
0077714cb0 chore: add note on data refresh in billing (#5938)
* chore: add note on data refresh in billing

* chore: add a class name and move these inline styles to the scss file

* chore: add light mode
2024-09-18 19:06:03 +05:30
Vishal Sharma
723c31f6c5 chore: hide usage explorer and update over 100rps warning (#5937) 2024-09-18 18:11:41 +05:30
Vikrant Gupta
1024483e58 fix: added safety check for query filter items (#6004)
* fix: added safety check for query filter items

* fix: added a bunch of missing safety nets
2024-09-18 18:02:17 +05:30
Vishal Sharma
cbcef2c880 chore: update calendly link (#5954) 2024-09-18 18:00:45 +05:30
Nityananda Gohain
0711c8855e fix: exists/nexists support for top level columns (#5990) 2024-09-18 11:51:13 +05:30
163 changed files with 8379 additions and 1895 deletions

View File

@@ -1,58 +1,49 @@
---
name: Request Dashboard
about: Request a new dashboard for the SigNoz Dashboards repository
title: ''
title: '[Dashboard Request] '
labels: 'dashboard-template'
assignees: ''
---
## 📝 Dashboard Request Template
<!-- Use this template to request a new dashboard for the SigNoz Dashboards repository. Providing detailed information will help us understand your needs better and speed up the dashboard creation process. -->
*Use this template to request a new dashboard for the SigNoz Dashboards repository. Please provide as much detail as possible to help us understand your needs.*
## Dashboard Name
---
<!-- Provide the name for the requested dashboard. Be specific (e.g., "MySQL Monitoring Dashboard"). -->
### 1. Dashboard Name
## Expected Dashboard Sections and Panels
Name of the requested dashboard (e.g., MySQL Monitoring Dashboard):
(Can be tweaked (add or remove panels/sections) according to available metrics)
---
### Section Name
### 2. Expected Dashboard Sections and Panels
<!-- Brief description of what this section should display (e.g., "Resource usage metrics for MySQL database"). -->
#### Section Name
### Panel Name
Brief description of the section (e.g., "Resource usage metrics for MySQL database").
<!-- Description of the panel (e.g., "Displays current CPU usage, memory usage, etc."). -->
#### Panel Name
Panel description (e.g., "Value-type panels displaying current CPU usage, memory usage, etc.").
- **Example:**
<!-- - **Example:**
- **Section**: Resource Metrics
- **Panel**: CPU Usage - Displays the current CPU usage across all database instances.
- **Panel**: Memory Usage - Displays the total memory used by the MySQL process.
- **Panel**: Memory Usage - Displays the total memory used by the MySQL process. -->
(Repeat this format for additional sections and panels)
<!-- Repeat this format for any additional sections or panels. -->
---
## Expected Dashboard Variables
### 3. Expected Variables
<!-- List any dashboard variables that should be included in the dashboard. Examples could be `deployment.environment`, `hostname`, `region`, etc. -->
List any variables you expect to use in the dashboard (e.g., `deployment.environment`, `hostname`, etc.).
## Additional Comments or Requirements
---
<!-- Include any other details, special requirements, or specific visualizations you'd like to request for this dashboard. -->
### 4. Additional Comments or Requirements
## References or Screenshots
Any additional details or special requirements for the dashboard?
<!-- Add any references or screenshots of requested dashboard if available. -->
---
### 📋 Notes
## 📋 Notes
Please review the [CONTRIBUTING.md](https://github.com/SigNoz/dashboards/blob/main/CONTRIBUTING.md) for guidelines on dashboard structure, naming conventions, and how to submit a pull request.
---
Thank you for your request! We will review it and provide feedback or guidance as necessary.

View File

@@ -30,6 +30,7 @@ Also, have a look at these [good first issues label](https://github.com/SigNoz/s
- [To run ClickHouse setup](#41-to-run-clickhouse-setup-recommended-for-local-development)
- [Contribute to SigNoz Helm Chart](#5-contribute-to-signoz-helm-chart-)
- [To run helm chart for local development](#51-to-run-helm-chart-for-local-development)
- [Contribute to Dashboards](#6-contribute-to-dashboards-)
- [Other Ways to Contribute](#other-ways-to-contribute)
# 1. General Instructions 📝
@@ -37,7 +38,7 @@ Also, have a look at these [good first issues label](https://github.com/SigNoz/s
## 1.1 For Creating Issue(s)
Before making any significant changes and before filing a new issue, please check [existing open](https://github.com/SigNoz/signoz/issues?q=is%3Aopen+is%3Aissue), or [recently closed](https://github.com/SigNoz/signoz/issues?q=is%3Aissue+is%3Aclosed) issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can.
**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy)
**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Request Dashboard](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=dashboard-template&projects=&template=request_dashboard.md&title=%5BDashboard+Request%5D+) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy)
#### Details like these are incredibly useful:
@@ -56,7 +57,7 @@ Before making any significant changes and before filing a new issue, please chec
Discussing your proposed changes ahead of time will make the contribution
process smooth for everyone 🙌.
**[`^top^`](#)**
**[`^top^`](#contributing-guidelines)**
<hr>
@@ -97,13 +98,14 @@ GitHub provides additional document on [forking a repository](https://help.githu
stability and quality of the component.
You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [SLACK](https://signoz.io/slack).
You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [slack community](https://signoz.io/slack).
### Pointers:
- If you find any **bugs** → please create an [**issue.**](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=)
- If you find anything **missing** in documentation → you can create an issue with the label **`documentation`**.
- If you want to build any **new feature** → please create an [issue with the label **`enhancement`**.](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=)
- If you want to **discuss** something about the product, start a new [**discussion**.](https://github.com/SigNoz/signoz/discussions)
- If you want to request a new **dashboard template** → please create an issue [here](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=dashboard-template&projects=&template=request_dashboard.md&title=%5BDashboard+Request%5D+).
<hr>
@@ -117,7 +119,7 @@ e.g. If you are submitting a fix for an issue in frontend, the PR name should be
- Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :)
**[`^top^`](#)**
**[`^top^`](#contributing-guidelines)**
<hr>
@@ -127,14 +129,13 @@ e.g. If you are submitting a fix for an issue in frontend, the PR name should be
- [**Frontend**](#3-develop-frontend-) (Written in Typescript, React)
- [**Backend**](#4-contribute-to-backend-query-service-) (Query Service, written in Go)
- [**Dashboard Templates**](#6-contribute-to-dashboards-) (JSON dashboard templates built with SigNoz)
Depending upon your area of expertise & interest, you can choose one or more to contribute. Below are detailed instructions to contribute in each area.
**Please note:** If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻
**Please note:** If you want to work on an issue, please add a brief description of your solution on the issue before starting work on it.
⚠️ If you just raise a PR, without the corresponding issue being assigned to you - it may not be accepted.
**[`^top^`](#)**
**[`^top^`](#contributing-guidelines)**
<hr>
@@ -188,7 +189,7 @@ Also, have a look at [Frontend README.md](https://github.com/SigNoz/signoz/blob/
### Important Notes:
The Maintainers / Contributors who will change Line Numbers of `Frontend` & `Query-Section`, please update line numbers in [`/.scripts/commentLinesForSetup.sh`](https://github.com/SigNoz/signoz/blob/develop/.scripts/commentLinesForSetup.sh)
**[`^top^`](#)**
**[`^top^`](#contributing-guidelines)**
## 3.2 Contribute to Frontend without installing SigNoz backend
@@ -209,7 +210,7 @@ Please ping us in the [`#contributing`](https://signoz-community.slack.com/archi
**Frontend should now be accessible at** [`http://localhost:3301/services`](http://localhost:3301/services)
**[`^top^`](#)**
**[`^top^`](#contributing-guidelines)**
<hr>
@@ -309,7 +310,7 @@ Click the button below. A workspace with all required environments will be creat
> To use it on your forked repo, edit the 'Open in Gitpod' button URL to `https://gitpod.io/#https://github.com/<your-github-username>/signoz` -->
**[`^top^`](#)**
**[`^top^`](#contributing-guidelines)**
<hr>
@@ -365,10 +366,21 @@ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-
| HOTROD_NAMESPACE=sample-application bash
```
**[`^top^`](#)**
**[`^top^`](#contributing-guidelines)**
---
# 6. Contribute to Dashboards 📈
**Need to Update: [https://github.com/SigNoz/dashboards](https://github.com/SigNoz/dashboards)**
To contribute a new dashboard template for any service, follow the contribution guidelines in the [Dashboard Contributing Guide](https://github.com/SigNoz/dashboards/blob/main/CONTRIBUTING.md). In brief:
1. Create a dashboard JSON file.
2. Add a README file explaining the dashboard, the metrics ingested, and the configurations needed.
3. Include screenshots of the dashboard in the `assets/` directory.
4. Submit a pull request for review.
## Other Ways to Contribute
There are many other ways to get involved with the community and to participate in this project:
@@ -379,7 +391,6 @@ There are many other ways to get involved with the community and to participate
- Help answer questions on forums such as Stack Overflow and [SigNoz Community Slack Channel](https://signoz.io/slack).
- Tell others about the project on Twitter, your blog, etc.
Again, Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :)
Thank You!

View File

@@ -146,11 +146,11 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.49.1
image: signoz/query-service:0.55.0
command:
[
"-config=/root/config/prometheus.yml",
# "--prefer-delta=true"
"--use-logs-new-schema=true"
]
# ports:
# - "6060:6060" # pprof port
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.48.0
image: signoz/frontend:0.55.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.10
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.10
deploy:
restart_policy:
condition: on-failure

View File

@@ -144,6 +144,7 @@ exporters:
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
use_new_schema: true
extensions:
health_check:
endpoint: 0.0.0.0:13133

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.10}
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.10
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -25,7 +25,7 @@ services:
command:
[
"-config=/root/config/prometheus.yml",
# "--prefer-delta=true"
"--use-logs-new-schema=true"
]
ports:
- "6060:6060"

View File

@@ -164,13 +164,13 @@ 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.55.0}
container_name: signoz-query-service
command:
[
"-config=/root/config/prometheus.yml",
"-gateway-url=https://api.staging.signoz.cloud"
# "--prefer-delta=true"
"-gateway-url=https://api.staging.signoz.cloud",
"--use-logs-new-schema=true"
]
# ports:
# - "6060:6060" # pprof port
@@ -204,7 +204,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
image: signoz/frontend:${DOCKER_TAG:-0.55.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.10}
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.10}
container_name: signoz-otel-collector
command:
[

View File

@@ -164,12 +164,12 @@ 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.55.0}
container_name: signoz-query-service
command:
[
"-config=/root/config/prometheus.yml"
# "--prefer-delta=true"
"-config=/root/config/prometheus.yml",
"--use-logs-new-schema=true"
]
# ports:
# - "6060:6060" # pprof port
@@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
image: signoz/frontend:${DOCKER_TAG:-0.55.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.10}
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.10}
container_name: signoz-otel-collector
command:
[

View File

@@ -154,6 +154,7 @@ exporters:
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
use_new_schema: true
# logging: {}
service:

View File

@@ -16,6 +16,10 @@ const (
SeasonalityWeekly Seasonality = "weekly"
)
func (s Seasonality) String() string {
return string(s)
}
var (
oneWeekOffset = 24 * 7 * time.Hour.Milliseconds()
oneDayOffset = 24 * time.Hour.Milliseconds()

View File

@@ -67,6 +67,7 @@ func (p *BaseSeasonalProvider) getQueryParams(req *GetAnomaliesRequest) *anomaly
}
func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQueryParams) (*anomalyQueryResults, error) {
zap.L().Info("fetching results for current period", zap.Any("currentPeriodQuery", params.CurrentPeriodQuery))
currentPeriodResults, _, err := p.querierV2.QueryRange(ctx, params.CurrentPeriodQuery)
if err != nil {
return nil, err
@@ -77,6 +78,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
return nil, err
}
zap.L().Info("fetching results for past period", zap.Any("pastPeriodQuery", params.PastPeriodQuery))
pastPeriodResults, _, err := p.querierV2.QueryRange(ctx, params.PastPeriodQuery)
if err != nil {
return nil, err
@@ -87,6 +89,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
return nil, err
}
zap.L().Info("fetching results for current season", zap.Any("currentSeasonQuery", params.CurrentSeasonQuery))
currentSeasonResults, _, err := p.querierV2.QueryRange(ctx, params.CurrentSeasonQuery)
if err != nil {
return nil, err
@@ -97,6 +100,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
return nil, err
}
zap.L().Info("fetching results for past season", zap.Any("pastSeasonQuery", params.PastSeasonQuery))
pastSeasonResults, _, err := p.querierV2.QueryRange(ctx, params.PastSeasonQuery)
if err != nil {
return nil, err
@@ -107,6 +111,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
return nil, err
}
zap.L().Info("fetching results for past 2 season", zap.Any("past2SeasonQuery", params.Past2SeasonQuery))
past2SeasonResults, _, err := p.querierV2.QueryRange(ctx, params.Past2SeasonQuery)
if err != nil {
return nil, err
@@ -117,6 +122,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
return nil, err
}
zap.L().Info("fetching results for past 3 season", zap.Any("past3SeasonQuery", params.Past3SeasonQuery))
past3SeasonResults, _, err := p.querierV2.QueryRange(ctx, params.Past3SeasonQuery)
if err != nil {
return nil, err
@@ -184,7 +190,7 @@ func (p *BaseSeasonalProvider) getMovingAvg(series *v3.Series, movingAvgWindowSi
return 0
}
if startIdx >= len(series.Points)-movingAvgWindowSize {
startIdx = len(series.Points) - movingAvgWindowSize
startIdx = int(math.Max(0, float64(len(series.Points)-movingAvgWindowSize)))
}
var sum float64
points := series.Points[startIdx:]
@@ -250,7 +256,7 @@ func (p *BaseSeasonalProvider) getPredictedSeries(
// moving avg of the previous period series + z score threshold * std dev of the series
// moving avg of the previous period series - z score threshold * std dev of the series
func (p *BaseSeasonalProvider) getBounds(
series, prevSeries, _, _, _, _ *v3.Series,
series, predictedSeries *v3.Series,
zScoreThreshold float64,
) (*v3.Series, *v3.Series) {
upperBoundSeries := &v3.Series{
@@ -266,8 +272,8 @@ func (p *BaseSeasonalProvider) getBounds(
}
for idx, curr := range series.Points {
upperBound := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx) + zScoreThreshold*p.getStdDev(series)
lowerBound := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx) - zScoreThreshold*p.getStdDev(series)
upperBound := p.getMovingAvg(predictedSeries, movingAvgWindowSize, idx) + zScoreThreshold*p.getStdDev(series)
lowerBound := p.getMovingAvg(predictedSeries, movingAvgWindowSize, idx) - zScoreThreshold*p.getStdDev(series)
upperBoundSeries.Points = append(upperBoundSeries.Points, v3.Point{
Timestamp: curr.Timestamp,
Value: upperBound,
@@ -431,11 +437,7 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, req *GetAnomali
upperBoundSeries, lowerBoundSeries := p.getBounds(
series,
pastPeriodSeries,
currentSeasonSeries,
pastSeasonSeries,
past2SeasonSeries,
past3SeasonSeries,
predictedSeries,
zScoreThreshold,
)
result.UpperBoundSeries = append(result.UpperBoundSeries, upperBoundSeries)

View File

@@ -177,6 +177,8 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
am.ViewAccess(ah.listLicensesV2)).
Methods(http.MethodGet)
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
// Gateway
router.PathPrefix(gateway.RoutePrefix).HandlerFunc(am.AdminAccess(ah.ServeGatewayHTTP))

View File

@@ -0,0 +1,119 @@
package api
import (
"bytes"
"fmt"
"io"
"net/http"
"go.signoz.io/signoz/ee/query-service/anomaly"
baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.uber.org/zap"
)
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
queryRangeParams, apiErrorObj := baseapp.ParseQueryRangeParams(r)
if apiErrorObj != nil {
zap.L().Error("error parsing metric query range params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil)
return
}
queryRangeParams.Version = "v4"
// add temporality for each metric
temporalityErr := aH.PopulateTemporality(r.Context(), queryRangeParams)
if temporalityErr != nil {
zap.L().Error("Error while adding temporality for metrics", zap.Error(temporalityErr))
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil)
return
}
anomalyQueryExists := false
anomalyQuery := &v3.BuilderQuery{}
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
for _, fn := range query.Functions {
if fn.Name == v3.FunctionNameAnomaly {
anomalyQueryExists = true
anomalyQuery = query
break
}
}
}
}
if anomalyQueryExists {
// ensure all queries have metric data source, and there should be only one anomaly query
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
if query.DataSource != v3.DataSourceMetrics {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("all queries must have metric data source")}, nil)
return
}
}
// get the threshold, and seasonality from the anomaly query
var seasonality anomaly.Seasonality
for _, fn := range anomalyQuery.Functions {
if fn.Name == v3.FunctionNameAnomaly {
seasonalityStr, ok := fn.NamedArgs["seasonality"].(string)
if !ok {
seasonalityStr = "daily"
}
if seasonalityStr == "weekly" {
seasonality = anomaly.SeasonalityWeekly
} else if seasonalityStr == "daily" {
seasonality = anomaly.SeasonalityDaily
} else {
seasonality = anomaly.SeasonalityHourly
}
break
}
}
var provider anomaly.Provider
switch seasonality {
case anomaly.SeasonalityWeekly:
provider = anomaly.NewWeeklyProvider(
anomaly.WithCache[*anomaly.WeeklyProvider](aH.opts.Cache),
anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.WeeklyProvider](aH.opts.DataConnector),
anomaly.WithFeatureLookup[*anomaly.WeeklyProvider](aH.opts.FeatureFlags),
)
case anomaly.SeasonalityDaily:
provider = anomaly.NewDailyProvider(
anomaly.WithCache[*anomaly.DailyProvider](aH.opts.Cache),
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.DailyProvider](aH.opts.DataConnector),
anomaly.WithFeatureLookup[*anomaly.DailyProvider](aH.opts.FeatureFlags),
)
case anomaly.SeasonalityHourly:
provider = anomaly.NewHourlyProvider(
anomaly.WithCache[*anomaly.HourlyProvider](aH.opts.Cache),
anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.HourlyProvider](aH.opts.DataConnector),
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](aH.opts.FeatureFlags),
)
}
anomalies, err := provider.GetAnomalies(r.Context(), &anomaly.GetAnomaliesRequest{Params: queryRangeParams})
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
uniqueResults := make(map[string]*v3.Result)
for _, anomaly := range anomalies.Results {
uniqueResults[anomaly.QueryName] = anomaly
uniqueResults[anomaly.QueryName].IsAnomaly = true
}
aH.Respond(w, uniqueResults)
} else {
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
aH.QueryRangeV4(w, r)
}
}

View File

@@ -170,6 +170,14 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
}
var c cache.Cache
if serverOptions.CacheConfigPath != "" {
cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath)
if err != nil {
return nil, err
}
c = cache.NewCache(cacheOpts)
}
<-readerReady
rm, err := makeRulesManager(serverOptions.PromConfigPath,
@@ -177,6 +185,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.RuleRepoURL,
localDB,
reader,
c,
serverOptions.DisableRules,
lm,
serverOptions.UseLogsNewSchema,
@@ -237,15 +246,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
telemetry.GetInstance().SetReader(reader)
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
var c cache.Cache
if serverOptions.CacheConfigPath != "" {
cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath)
if err != nil {
return nil, err
}
c = cache.NewCache(cacheOpts)
}
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
if err != nil {
@@ -732,6 +732,7 @@ func makeRulesManager(
ruleRepoURL string,
db *sqlx.DB,
ch baseint.Reader,
cache cache.Cache,
disableRules bool,
fm baseint.FeatureLookup,
useLogsNewSchema bool) (*baserules.Manager, error) {
@@ -760,6 +761,7 @@ func makeRulesManager(
DisableRules: disableRules,
FeatureFlags: fm,
Reader: ch,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,

View File

@@ -13,7 +13,6 @@ const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
const QueryBuilderSearchV2 = "QUERY_BUILDER_SEARCH_V2"
var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{
@@ -129,7 +128,7 @@ var BasicPlan = basemodel.FeatureSet{
Route: "",
},
basemodel.Feature{
Name: QueryBuilderSearchV2,
Name: basemodel.AnomalyDetection,
Active: false,
Usage: 0,
UsageLimit: -1,
@@ -244,8 +243,8 @@ var ProPlan = basemodel.FeatureSet{
Route: "",
},
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Name: basemodel.AnomalyDetection,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
@@ -373,8 +372,8 @@ var EnterprisePlan = basemodel.FeatureSet{
Route: "",
},
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Name: basemodel.AnomalyDetection,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",

View File

@@ -0,0 +1,393 @@
package rules
import (
"context"
"encoding/json"
"fmt"
"math"
"strings"
"sync"
"time"
"go.uber.org/zap"
"go.signoz.io/signoz/ee/query-service/anomaly"
"go.signoz.io/signoz/pkg/query-service/cache"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/model"
querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2"
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
"go.signoz.io/signoz/pkg/query-service/interfaces"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/utils/labels"
"go.signoz.io/signoz/pkg/query-service/utils/times"
"go.signoz.io/signoz/pkg/query-service/utils/timestamp"
"go.signoz.io/signoz/pkg/query-service/formatter"
baserules "go.signoz.io/signoz/pkg/query-service/rules"
yaml "gopkg.in/yaml.v2"
)
const (
RuleTypeAnomaly = "anomaly_rule"
)
type AnomalyRule struct {
*baserules.BaseRule
mtx sync.Mutex
reader interfaces.Reader
// querierV2 is used for alerts created after the introduction of new metrics query builder
querierV2 interfaces.Querier
provider anomaly.Provider
seasonality anomaly.Seasonality
}
func NewAnomalyRule(
id string,
p *baserules.PostableRule,
featureFlags interfaces.FeatureLookup,
reader interfaces.Reader,
cache cache.Cache,
opts ...baserules.RuleOption,
) (*AnomalyRule, error) {
zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts))
baseRule, err := baserules.NewBaseRule(id, p, reader, opts...)
if err != nil {
return nil, err
}
t := AnomalyRule{
BaseRule: baseRule,
}
switch strings.ToLower(p.RuleCondition.Seasonality) {
case "hourly":
t.seasonality = anomaly.SeasonalityHourly
case "daily":
t.seasonality = anomaly.SeasonalityDaily
case "weekly":
t.seasonality = anomaly.SeasonalityWeekly
default:
t.seasonality = anomaly.SeasonalityDaily
}
zap.L().Info("using seasonality", zap.String("seasonality", t.seasonality.String()))
querierOptsV2 := querierV2.QuerierOptions{
Reader: reader,
Cache: cache,
KeyGenerator: queryBuilder.NewKeyGenerator(),
FeatureLookup: featureFlags,
}
t.querierV2 = querierV2.NewQuerier(querierOptsV2)
t.reader = reader
if t.seasonality == anomaly.SeasonalityHourly {
t.provider = anomaly.NewHourlyProvider(
anomaly.WithCache[*anomaly.HourlyProvider](cache),
anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.HourlyProvider](reader),
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](featureFlags),
)
} else if t.seasonality == anomaly.SeasonalityDaily {
t.provider = anomaly.NewDailyProvider(
anomaly.WithCache[*anomaly.DailyProvider](cache),
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.DailyProvider](reader),
anomaly.WithFeatureLookup[*anomaly.DailyProvider](featureFlags),
)
} else if t.seasonality == anomaly.SeasonalityWeekly {
t.provider = anomaly.NewWeeklyProvider(
anomaly.WithCache[*anomaly.WeeklyProvider](cache),
anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.WeeklyProvider](reader),
anomaly.WithFeatureLookup[*anomaly.WeeklyProvider](featureFlags),
)
}
return &t, nil
}
func (r *AnomalyRule) Type() baserules.RuleType {
return RuleTypeAnomaly
}
func (r *AnomalyRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, error) {
zap.L().Info("prepareQueryRange", zap.Int64("ts", ts.UnixMilli()), zap.Int64("evalWindow", r.EvalWindow().Milliseconds()), zap.Int64("evalDelay", r.EvalDelay().Milliseconds()))
start := ts.Add(-time.Duration(r.EvalWindow())).UnixMilli()
end := ts.UnixMilli()
if r.EvalDelay() > 0 {
start = start - int64(r.EvalDelay().Milliseconds())
end = end - int64(r.EvalDelay().Milliseconds())
}
// round to minute otherwise we could potentially miss data
start = start - (start % (60 * 1000))
end = end - (end % (60 * 1000))
compositeQuery := r.Condition().CompositeQuery
if compositeQuery.PanelType != v3.PanelTypeGraph {
compositeQuery.PanelType = v3.PanelTypeGraph
}
// default mode
return &v3.QueryRangeParamsV3{
Start: start,
End: end,
Step: int64(math.Max(float64(common.MinAllowedStepInterval(start, end)), 60)),
CompositeQuery: compositeQuery,
Variables: make(map[string]interface{}, 0),
NoCache: false,
}, nil
}
func (r *AnomalyRule) GetSelectedQuery() string {
return r.Condition().GetSelectedQueryName()
}
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (baserules.Vector, error) {
params, err := r.prepareQueryRange(ts)
if err != nil {
return nil, err
}
err = r.PopulateTemporality(ctx, params)
if err != nil {
return nil, fmt.Errorf("internal error while setting temporality")
}
anomalies, err := r.provider.GetAnomalies(ctx, &anomaly.GetAnomaliesRequest{
Params: params,
Seasonality: r.seasonality,
})
if err != nil {
return nil, err
}
var queryResult *v3.Result
for _, result := range anomalies.Results {
if result.QueryName == r.GetSelectedQuery() {
queryResult = result
break
}
}
var resultVector baserules.Vector
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON)))
for _, series := range queryResult.AnomalyScores {
smpl, shouldAlert := r.ShouldAlert(*series)
if shouldAlert {
resultVector = append(resultVector, smpl)
}
}
return resultVector, nil
}
func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, error) {
prevState := r.State()
valueFormatter := formatter.FromUnit(r.Unit())
res, err := r.buildAndRunQuery(ctx, ts)
if err != nil {
return nil, err
}
r.mtx.Lock()
defer r.mtx.Unlock()
resultFPs := map[uint64]struct{}{}
var alerts = make(map[uint64]*baserules.Alert, len(res))
for _, smpl := range res {
l := make(map[string]string, len(smpl.Metric))
for _, lbl := range smpl.Metric {
l[lbl.Name] = lbl.Value
}
value := valueFormatter.Format(smpl.V, r.Unit())
threshold := valueFormatter.Format(r.TargetVal(), r.Unit())
zap.L().Debug("Alert template data for rule", zap.String("name", r.Name()), zap.String("formatter", valueFormatter.Name()), zap.String("value", value), zap.String("threshold", threshold))
tmplData := baserules.AlertTemplateData(l, value, threshold)
// Inject some convenience variables that are easier to remember for users
// who are not used to Go's templating system.
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
// utility function to apply go template on labels and annotations
expand := func(text string) string {
tmpl := baserules.NewTemplateExpander(
ctx,
defs+text,
"__alert_"+r.Name(),
tmplData,
times.Time(timestamp.FromTime(ts)),
nil,
)
result, err := tmpl.Expand()
if err != nil {
result = fmt.Sprintf("<error expanding template: %s>", err)
zap.L().Error("Expanding alert template failed", zap.Error(err), zap.Any("data", tmplData))
}
return result
}
lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel)
resultLabels := labels.NewBuilder(smpl.MetricOrig).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels()
for name, value := range r.Labels().Map() {
lb.Set(name, expand(value))
}
lb.Set(labels.AlertNameLabel, r.Name())
lb.Set(labels.AlertRuleIdLabel, r.ID())
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
annotations := make(labels.Labels, 0, len(r.Annotations().Map()))
for name, value := range r.Annotations().Map() {
annotations = append(annotations, labels.Label{Name: common.NormalizeLabelName(name), Value: expand(value)})
}
if smpl.IsMissing {
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())
}
lbs := lb.Labels()
h := lbs.Hash()
resultFPs[h] = struct{}{}
if _, ok := alerts[h]; ok {
zap.L().Error("the alert query returns duplicate records", zap.String("ruleid", r.ID()), zap.Any("alert", alerts[h]))
err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
return nil, err
}
alerts[h] = &baserules.Alert{
Labels: lbs,
QueryResultLables: resultLabels,
Annotations: annotations,
ActiveAt: ts,
State: model.StatePending,
Value: smpl.V,
GeneratorURL: r.GeneratorURL(),
Receivers: r.PreferredChannels(),
Missing: smpl.IsMissing,
}
}
zap.L().Info("number of alerts found", zap.String("name", r.Name()), zap.Int("count", len(alerts)))
// alerts[h] is ready, add or update active list now
for h, a := range alerts {
// Check whether we already have alerting state for the identifying label set.
// Update the last value and annotations if so, create a new alert entry otherwise.
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
alert.Value = a.Value
alert.Annotations = a.Annotations
alert.Receivers = r.PreferredChannels()
continue
}
r.Active[h] = a
}
itemsToAdd := []model.RuleStateHistory{}
// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
for fp, a := range r.Active {
labelsJSON, err := json.Marshal(a.QueryResultLables)
if err != nil {
zap.L().Error("error marshaling labels", zap.Error(err), zap.Any("labels", a.Labels))
}
if _, ok := resultFPs[fp]; !ok {
// If the alert was previously firing, keep it around for a given
// retention time so it is reported as resolved to the AlertManager.
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > baserules.ResolvedRetention) {
delete(r.Active, fp)
}
if a.State != model.StateInactive {
a.State = model.StateInactive
a.ResolvedAt = ts
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
RuleID: r.ID(),
RuleName: r.Name(),
State: model.StateInactive,
StateChanged: true,
UnixMilli: ts.UnixMilli(),
Labels: model.LabelsString(labelsJSON),
Fingerprint: a.QueryResultLables.Hash(),
Value: a.Value,
})
}
continue
}
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration() {
a.State = model.StateFiring
a.FiredAt = ts
state := model.StateFiring
if a.Missing {
state = model.StateNoData
}
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
RuleID: r.ID(),
RuleName: r.Name(),
State: state,
StateChanged: true,
UnixMilli: ts.UnixMilli(),
Labels: model.LabelsString(labelsJSON),
Fingerprint: a.QueryResultLables.Hash(),
Value: a.Value,
})
}
}
currentState := r.State()
overallStateChanged := currentState != prevState
for idx, item := range itemsToAdd {
item.OverallStateChanged = overallStateChanged
item.OverallState = currentState
itemsToAdd[idx] = item
}
r.RecordRuleStateHistory(ctx, prevState, currentState, itemsToAdd)
return len(r.Active), nil
}
func (r *AnomalyRule) String() string {
ar := baserules.PostableRule{
AlertName: r.Name(),
RuleCondition: r.Condition(),
EvalWindow: baserules.Duration(r.EvalWindow()),
Labels: r.Labels().Map(),
Annotations: r.Annotations().Map(),
PreferredChannels: r.PreferredChannels(),
}
byt, err := yaml.Marshal(ar)
if err != nil {
return fmt.Sprintf("error marshaling alerting rule: %s", err.Error())
}
return string(byt)
}

View File

@@ -53,6 +53,25 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
// create promql rule task for evalution
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
} else if opts.Rule.RuleType == baserules.RuleTypeAnomaly {
// create anomaly rule
ar, err := NewAnomalyRule(
ruleId,
opts.Rule,
opts.FF,
opts.Reader,
opts.Cache,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
)
if err != nil {
return task, err
}
rules = append(rules, ar)
// create anomaly rule task for evalution
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
} else {
return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", baserules.RuleTypeProm, baserules.RuleTypeThreshold)
}

View File

@@ -1,11 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2048_2251)">
<path opacity="0.9" d="M8.02226 15.9866C3.56539 15.9866 -6.10352e-05 12.4896 -6.10352e-05 8.11832C-6.10352e-05 3.79075 3.56539 0.25 8.02226 0.25H13.0584C14.7075 0.25 15.9999 1.56139 15.9999 3.13506V8.11832C15.9999 12.4896 12.4345 15.9866 8.02226 15.9866Z" fill="#F25733"/>
<path d="M7.95919 4.71207C4.63025 4.71207 2.75514 7.46868 2.67693 7.58603C2.48413 7.87508 2.48413 8.24888 2.67707 8.53816C2.75514 8.65528 4.63025 11.4119 7.95919 11.4119C11.2881 11.4119 13.1633 8.65528 13.2414 8.53792C13.4342 8.24888 13.4342 7.87508 13.2413 7.58582C13.1632 7.46868 11.2881 4.71207 7.95919 4.71207ZM3.13771 8.23088C3.06925 8.12832 3.06925 7.99571 3.13771 7.89307C3.20059 7.79867 4.53564 5.83764 6.92256 5.36723C5.84092 5.78476 5.07127 6.83485 5.07127 8.062C5.07127 9.28912 5.84092 10.3392 6.92256 10.7567C4.53564 10.2863 3.20059 8.32528 3.13771 8.23088ZM6.62838 8.062C6.62838 8.21488 6.50443 8.3388 6.35151 8.3388C6.19859 8.3388 6.07465 8.21488 6.07465 8.062C6.07465 7.02287 6.92003 6.17748 7.95916 6.17748C8.11207 6.17748 8.23599 6.30141 8.23599 6.45434C8.23599 6.60727 8.11207 6.73119 7.95916 6.73119C7.22535 6.73119 6.62838 7.32815 6.62838 8.062ZM7.95919 8.73504C7.58803 8.73504 7.2861 8.43312 7.2861 8.062C7.2861 7.69085 7.58803 7.3889 7.95919 7.3889C8.33039 7.3889 8.63231 7.69083 8.63231 8.062C8.63231 8.43312 8.33039 8.73504 7.95919 8.73504ZM12.7806 8.23088C12.7178 8.32528 11.3827 10.2863 8.99583 10.7567C10.0775 10.3392 10.8471 9.28912 10.8471 8.062C10.8471 6.83487 10.0775 5.78477 8.99583 5.36724C11.3827 5.83768 12.7178 7.7987 12.7806 7.89307C12.8491 7.99571 12.8491 8.12832 12.7806 8.23088Z" fill="#F9F2F9"/>
</g>
<defs>
<clipPath id="clip0_2048_2251">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><rect width="100" height="100" fill="url(#a)" rx="20"/><g fill="#fff" fill-rule="evenodd" clip-rule="evenodd" filter="url(#b)"><path d="M11 49.941v-.003l.002-.005.003-.014.007-.035a8.37 8.37 0 0 1 .105-.42c.073-.263.184-.624.348-1.072.328-.896.866-2.135 1.73-3.617 1.732-2.97 4.753-6.883 9.95-10.955 5.223-4.092 10.295-6.293 14.08-7.471a35.328 35.328 0 0 1 4.585-1.114 23.628 23.628 0 0 1 1.687-.223 9.17 9.17 0 0 1 .108-.009l.034-.002h.011l.007-.001s.002 0 .133 2.217c.13 2.218.132 2.218.132 2.218h-.002l-.053.004a19.098 19.098 0 0 0-1.326.178c-.809.136-1.937.37-3.302.763l-.127.043c-2.745.94-6.666 2.775-11.249 6.362-4.572 3.577-7.142 6.95-8.563 9.393-.711 1.222-1.137 2.215-1.383 2.889a9.995 9.995 0 0 0-.29.933c.008.037.022.095.044.173.046.166.123.423.246.76.246.674.672 1.667 1.383 2.89 1.421 2.441 3.991 5.815 8.563 9.392 4.584 3.587 8.504 5.423 11.25 6.362l.126.043c1.365.393 2.493.627 3.302.763a19.098 19.098 0 0 0 1.326.178l.053.004h.002s-.002 0-.133 2.218C43.66 75 43.657 75 43.657 75h-.007l-.011-.001-.034-.002a9.17 9.17 0 0 1-.478-.046 23.628 23.628 0 0 1-1.317-.186 35.328 35.328 0 0 1-4.584-1.114c-3.786-1.178-8.858-3.38-14.081-7.471-5.197-4.072-8.218-7.985-9.95-10.955-.864-1.482-1.402-2.72-1.73-3.617-.164-.448-.275-.81-.348-1.072a8.37 8.37 0 0 1-.105-.42l-.007-.035-.003-.014-.002-.005v-.121Zm78 0v-.003l-.002-.005-.002-.014-.008-.035a8.532 8.532 0 0 0-.105-.42 14.049 14.049 0 0 0-.348-1.072c-.328-.896-.866-2.135-1.73-3.617-1.732-2.97-4.753-6.883-9.95-10.955-5.223-4.092-10.295-6.293-14.08-7.471a35.328 35.328 0 0 0-4.585-1.114 23.628 23.628 0 0 0-1.687-.223 9.17 9.17 0 0 0-.108-.009l-.034-.002h-.011L56.343 25s-.002 0-.133 2.217c-.13 2.218-.132 2.218-.132 2.218h.002l.053.004a19.098 19.098 0 0 1 1.326.178c.809.136 1.937.37 3.302.763l.127.043c2.745.94 6.666 2.775 11.249 6.362 4.572 3.577 7.141 6.95 8.563 9.393.711 1.222 1.137 2.215 1.383 2.889a9.995 9.995 0 0 1 .29.933 9.995 9.995 0 0 1-.29.934c-.246.673-.672 1.666-1.383 2.888-1.422 2.442-3.991 5.816-8.563 9.393-4.584 3.587-8.504 5.423-11.25 6.362l-.126.043a30.108 30.108 0 0 1-3.302.763 19.098 19.098 0 0 1-1.326.178l-.053.004h-.002s.002 0 .133 2.218C56.34 75 56.343 75 56.343 75h.007l.011-.001.034-.002a9.17 9.17 0 0 0 .478-.046c.314-.034.758-.092 1.317-.186a35.328 35.328 0 0 0 4.584-1.114c3.786-1.178 8.858-3.38 14.081-7.471 5.197-4.072 8.218-7.985 9.95-10.955.864-1.482 1.402-2.72 1.73-3.617.164-.448.275-.81.348-1.072a8.532 8.532 0 0 0 .105-.42l.008-.035.002-.014.001-.005.001-.003v-.118Z"/><path d="M68.342 49.998c0 9.846-7.924 17.827-17.7 17.827-9.775 0-17.7-7.981-17.7-17.827 0-9.846 7.925-17.827 17.7-17.827 9.776 0 17.7 7.981 17.7 17.827ZM46.218 39.97s-2.127 2.508-2.766 4.457c-.412 1.257-.553 3.343-.553 3.343h-5.531s0-1.672 1.106-4.457c1.106-2.786 2.212-3.343 2.212-3.343h5.532Zm-2.766 15.6c.639 1.949 2.766 4.457 2.766 4.457h-5.532s-1.106-.557-2.212-3.343c-1.106-2.785-1.106-4.457-1.106-4.457h5.53s.142 2.086.554 3.343Z"/></g><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="matrix(40.99997 42 -42 40.99997 50 50)" gradientUnits="userSpaceOnUse"><stop offset=".33" stop-color="#F76526"/><stop offset="1" stop-color="#F43030"/></radialGradient><filter id="b" width="90" height="62" x="5" y="23" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="4"/><feGaussianBlur stdDeviation="3"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0.368384 0 0 0 0 0.0623777 0 0 0 0 0.0623777 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_3909_18731"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_3909_18731" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="4"/><feGaussianBlur stdDeviation="3"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/><feBlend in2="shape" result="effect2_innerShadow_3909_18731"/></filter></defs></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -53,6 +53,7 @@
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",

View File

@@ -40,6 +40,7 @@
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",

View File

@@ -1,3 +1,3 @@
{
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support."
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support or "
}

View File

@@ -53,6 +53,7 @@
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",

View File

@@ -40,6 +40,7 @@
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",

View File

@@ -1,3 +1,3 @@
{
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support."
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support or "
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -12,6 +12,7 @@ import useAnalytics from 'hooks/analytics/useAnalytics';
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode';
import { THEME_MODE } from 'hooks/useDarkMode/constant';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { NotificationProvider } from 'hooks/useNotifications';
@@ -58,23 +59,16 @@ function App(): JSX.Element {
const isDarkMode = useIsDarkMode();
const isOnboardingEnabled =
useFeatureFlags(FeatureKeys.ONBOARDING)?.active || false;
const isChatSupportEnabled =
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
const isPremiumSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const featureResponse = useGetFeatureFlag((allFlags) => {
const isOnboardingEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.ONBOARDING)?.active ||
false;
const isChatSupportEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
false;
const isPremiumSupportEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)?.active ||
false;
const showAddCreditCardModal =
!isPremiumSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
dispatch({
type: UPDATE_FEATURE_FLAG_RESPONSE,
payload: {
@@ -90,16 +84,6 @@ function App(): JSX.Element {
setRoutes(newRoutes);
}
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.Intercom('boot', {
app_id: process.env.INTERCOM_APP_ID,
email: user?.email || '',
name: user?.name || '',
});
}
});
const isOnBasicPlan =
@@ -201,6 +185,26 @@ function App(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
useEffect(() => {
const showAddCreditCardModal =
!isPremiumSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
window.Intercom('boot', {
app_id: process.env.INTERCOM_APP_ID,
email: user?.email || '',
name: user?.name || '',
});
}
}, [
isLoggedInState,
isChatSupportEnabled,
user,
licenseData,
isPremiumSupportEnabled,
]);
useEffect(() => {
if (user && user?.email && user?.userId && user?.name) {
try {

View File

@@ -12,6 +12,20 @@ beforeAll(() => {
matchMedia();
});
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),

View File

@@ -2,6 +2,14 @@ export const VIEW_TYPES = {
OVERVIEW: 'OVERVIEW',
JSON: 'JSON',
CONTEXT: 'CONTEXT',
INFRAMETRICS: 'INFRAMETRICS',
} as const;
export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES];
export const RESOURCE_KEYS = {
CLUSTER_NAME: 'k8s.cluster.name',
POD_NAME: 'k8s.pod.name',
NODE_NAME: 'k8s.node.name',
HOST_NAME: 'host.name',
} as const;

View File

@@ -9,6 +9,7 @@ import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { LOCALSTORAGE } from 'constants/localStorage';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import {
@@ -22,6 +23,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import {
BarChart2,
Braces,
Copy,
Filter,
@@ -36,7 +38,7 @@ 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 { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces';
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
@@ -192,6 +194,17 @@ function LogDetail({
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>
{selectedView === VIEW_TYPES.JSON && (
@@ -246,6 +259,15 @@ function LogDetail({
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
logLineTimestamp={log.timestamp.toString()}
/>
)}
</Drawer>
);
}

View File

@@ -22,26 +22,21 @@
}
&.INFO {
background-color: var(--bg-slate-400);
background-color: var(--bg-robin-500);
}
&.WARNING,
&.WARN {
background-color: var(--bg-amber-500);
}
&.ERROR {
background-color: var(--bg-cherry-500);
}
&.TRACE {
background-color: var(--bg-robin-300);
background-color: var(--bg-forest-400);
}
&.DEBUG {
background-color: var(--bg-forest-500);
background-color: var(--bg-aqua-500);
}
&.FATAL {
background-color: var(--bg-sakura-500);
}

View File

@@ -16,7 +16,7 @@ function WelcomeLeftContainer({
<Container>
<LeftContainer direction="vertical">
<Space align="center">
<Logo src="signoz-signup.svg" alt="logo" />
<Logo src="/Logos/signoz-brand-logo.svg" alt="logo" />
<Title style={{ fontSize: '46px', margin: 0 }}>SigNoz</Title>
</Space>
<Typography>{t('monitor_signup')}</Typography>

View File

@@ -6,7 +6,6 @@ export const AUTH0_REDIRECT_PATH = '/redirect';
export const DEFAULT_AUTH0_APP_REDIRECTION_PATH = ROUTES.APPLICATION;
export const IS_SIDEBAR_COLLAPSED = 'isSideBarCollapsed';
export const INVITE_MEMBERS_HASH = '#invite-team-members';
export const SIGNOZ_UPGRADE_PLAN_URL =

View File

@@ -1,8 +1,4 @@
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
const userOS = getUserOperatingSystem();
export const GlobalShortcuts = {
SidebarCollapse: '\\+meta',
NavigateToServices: 's+shift',
NavigateToTraces: 't+shift',
NavigateToLogs: 'l+shift',
@@ -13,7 +9,6 @@ export const GlobalShortcuts = {
};
export const GlobalShortcutsName = {
SidebarCollapse: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+\\`,
NavigateToServices: 'shift+s',
NavigateToTraces: 'shift+t',
NavigateToLogs: 'shift+l',
@@ -24,7 +19,6 @@ export const GlobalShortcutsName = {
};
export const GlobalShortcutsDescription = {
SidebarCollapse: 'Collpase the sidebar',
NavigateToServices: 'Navigate to Services page',
NavigateToTraces: 'Navigate to Traces page',
NavigateToLogs: 'Navigate to logs page',

View File

@@ -16,12 +16,6 @@
width: 100%;
}
}
&.docked {
.app-content {
width: calc(100% - 240px);
}
}
}
.chat-support-gateway {

View File

@@ -5,13 +5,11 @@ import './AppLayout.styles.scss';
import * as Sentry from '@sentry/react';
import { Flex } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion';
import cx from 'classnames';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import SideNav from 'container/SideNav';
@@ -22,22 +20,13 @@ import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import {
ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import { sideBarCollapse } from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
@@ -59,10 +48,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
(state) => state.app,
);
const [collapsed, setCollapsed] = useState<boolean>(
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
);
const { notifications } = useNotifications();
const isDarkMode = useIsDarkMode();
@@ -117,14 +102,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const latestCurrentCounter = useRef(0);
const latestVersionCounter = useRef(0);
const onCollapse = useCallback(() => {
setCollapsed((collapsed) => !collapsed);
}, []);
useLayoutEffect(() => {
dispatch(sideBarCollapse(collapsed));
}, [collapsed, dispatch]);
useEffect(() => {
if (
getUserLatestVersionResponse.isFetched &&
@@ -279,23 +256,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
}, [isDarkMode]);
const isSideNavCollapsed = getLocalStorageKey(IS_SIDEBAR_COLLAPSED);
/**
* Note: Right now we don't have a page-level method to pass the sidebar collapse state.
* Since the use case for overriding is not widely needed, we are setting it here
* so that the workspace locked page will have an expanded sidebar regardless of how users
* have set it or what is stored in localStorage. This will not affect the localStorage config.
*/
const isWorkspaceLocked = pathname === ROUTES.WORKSPACE_LOCKED;
return (
<Layout
className={cx(
isDarkMode ? 'darkMode' : 'lightMode',
isSideNavCollapsed ? 'sidebarCollapsed' : '',
)}
>
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
<Helmet>
<title>{pageTitle}</title>
</Helmet>
@@ -321,25 +283,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
)}
<Flex
className={cx(
'app-layout',
isDarkMode ? 'darkMode' : 'lightMode',
!collapsed && !renderFullScreen ? 'docked' : '',
)}
>
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
{isToDisplayLayout && !renderFullScreen && (
<SideNav
licenseData={licenseData}
isFetching={isFetching}
onCollapse={onCollapse}
collapsed={isWorkspaceLocked ? false : collapsed}
/>
<SideNav licenseData={licenseData} isFetching={isFetching} />
)}
<div
className={cx('app-content', collapsed ? 'collapsed' : '')}
data-overlayscrollbars-initialize
>
<div className="app-content" data-overlayscrollbars-initialize>
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize>
<OverlayScrollbar>

View File

@@ -50,6 +50,13 @@
align-items: center;
}
}
.billing-update-note {
text-align: left;
font-size: 13px;
color: var(--bg-vanilla-200);
margin-top: 16px;
}
}
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
@@ -75,5 +82,9 @@
}
}
}
.billing-update-note {
color: var(--bg-ink-200);
}
}
}

View File

@@ -348,7 +348,12 @@ export default function BillingContainer(): JSX.Element {
const BillingUsageGraphCallback = useCallback(
() =>
!isLoading && !isFetchingBillingData ? (
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
<>
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
<div className="billing-update-note">
Note: Billing metrics are updated once every 24 hours.
</div>
</>
) : (
<Card className="empty-graph-card" bordered={false}>
<Spinner size="large" tip="Loading..." height="35vh" />

View File

@@ -103,6 +103,7 @@ function RuleOptions({
<Select.Option value="2">{t('option_allthetimes')}</Select.Option>
<Select.Option value="3">{t('option_onaverage')}</Select.Option>
<Select.Option value="4">{t('option_intotal')}</Select.Option>
<Select.Option value="5">{t('option_last')}</Select.Option>
</InlineSelect>
);

View File

@@ -370,7 +370,10 @@ function FormAlertRules({
});
// invalidate rule in cache
ruleCache.invalidateQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
ruleCache.invalidateQueries([
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
`${ruleId}`,
]);
// eslint-disable-next-line sonarjs/no-identical-functions
setTimeout(() => {

View File

@@ -15,6 +15,13 @@
box-sizing: border-box;
margin: 16px 0;
border-radius: 3px;
.global-search {
.ant-input-group-addon {
border: none;
background-color: var(--bg-ink-300);
}
}
}
.height-widget {
@@ -55,3 +62,15 @@
}
}
}
.lightMode {
.full-view-container {
.graph-container {
.global-search {
.ant-input-group-addon {
background-color: var(--bg-vanilla-200);
}
}
}
}
}

View File

@@ -1,7 +1,11 @@
import './WidgetFullView.styles.scss';
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
import { Button, Spin } from 'antd';
import {
LoadingOutlined,
SearchOutlined,
SyncOutlined,
} from '@ant-design/icons';
import { Button, Input, Spin } from 'antd';
import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types';
import Spinner from 'components/Spinner';
@@ -140,7 +144,7 @@ function FullView({
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
boolean[]
>(Array(response.data?.payload.data.result.length).fill(true));
>(Array(response.data?.payload?.data?.result?.length).fill(true));
useEffect(() => {
const {
@@ -172,6 +176,10 @@ function FullView({
const isListView = widget.panelTypes === PANEL_TYPES.LIST;
const isTablePanel = widget.panelTypes === PANEL_TYPES.TABLE;
const [searchTerm, setSearchTerm] = useState<string>('');
if (response.isLoading && widget.panelTypes !== PANEL_TYPES.LIST) {
return <Spinner height="100%" size="large" tip="Loading..." />;
}
@@ -216,6 +224,18 @@ function FullView({
}}
isGraphLegendToggleAvailable={canModifyChart}
>
{isTablePanel && (
<Input
addonBefore={<SearchOutlined size={14} />}
className="global-search"
placeholder="Search..."
allowClear
key={widget.id}
onChange={(e): void => {
setSearchTerm(e.target.value || '');
}}
/>
)}
<PanelWrapper
queryResponse={response}
widget={widget}
@@ -226,6 +246,7 @@ function FullView({
graphVisibility={graphsVisibilityStates}
onDragSelect={onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
searchTerm={searchTerm}
/>
</GraphContainer>
</div>

View File

@@ -234,6 +234,8 @@ function WidgetGraphComponent({
});
};
const [searchTerm, setSearchTerm] = useState<string>('');
const loadingState =
(queryResponse.isLoading || queryResponse.status === 'idle') &&
widget.panelTypes !== PANEL_TYPES.LIST;
@@ -317,6 +319,7 @@ function WidgetGraphComponent({
isWarning={isWarning}
isFetchingResponse={isFetchingResponse}
tableProcessedDataRef={tableProcessedDataRef}
setSearchTerm={setSearchTerm}
/>
</div>
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
@@ -337,6 +340,7 @@ function WidgetGraphComponent({
onDragSelect={onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
searchTerm={searchTerm}
/>
</div>
)}

View File

@@ -11,6 +11,7 @@ import { isEqual } from 'lodash-es';
import isEmpty from 'lodash-es/isEmpty';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
@@ -48,6 +49,7 @@ function GridCardGraph({
AppState,
GlobalReducer
>((state) => state.globalTime);
const queryClient = useQueryClient();
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
@@ -136,6 +138,25 @@ function GridCardGraph({
};
});
// TODO [vikrantgupta25] remove this useEffect with refactor as this is prone to race condition
// this is added to tackle the case of async communication between VariableItem.tsx and GridCard.tsx
useEffect(() => {
if (variablesToGetUpdated.length > 0) {
queryClient.cancelQueries([
maxTime,
minTime,
globalSelectedInterval,
variables,
widget?.query,
widget?.panelTypes,
widget.timePreferance,
widget.fillSpans,
requestData,
]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variablesToGetUpdated]);
useEffect(() => {
if (!isEqual(updatedQuery, requestData.query)) {
setRequestData((prev) => ({

View File

@@ -2,7 +2,7 @@
display: flex;
justify-content: space-between;
align-items: center;
height: 30px;
height: 36px;
width: 100%;
padding: 0.5rem;
box-sizing: border-box;
@@ -10,6 +10,14 @@
font-weight: 600;
cursor: move;
.ant-input-group-addon {
border: none;
background-color: var(--bg-ink-500);
}
.search-header-icons {
cursor: pointer;
}
}
.widget-header-title {
@@ -19,6 +27,7 @@
.widget-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.widget-header-more-options {
visibility: hidden;
@@ -30,6 +39,10 @@
padding: 8px;
}
.widget-header-more-options-visible {
visibility: visible;
}
.widget-header-hover {
visibility: visible;
}
@@ -37,3 +50,11 @@
.widget-api-actions {
padding-right: 0.25rem;
}
.lightMode {
.widget-header-container {
.ant-input-group-addon {
background-color: inherit;
}
}
}

View File

@@ -9,9 +9,10 @@ import {
ExclamationCircleOutlined,
FullscreenOutlined,
MoreOutlined,
SearchOutlined,
WarningOutlined,
} from '@ant-design/icons';
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
import { Dropdown, Input, MenuProps, Tooltip, Typography } from 'antd';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -20,8 +21,9 @@ import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty } from 'lodash-es';
import { X } from 'lucide-react';
import { unparse } from 'papaparse';
import { ReactNode, useCallback, useMemo } from 'react';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -51,6 +53,7 @@ interface IWidgetHeaderProps {
isWarning: boolean;
isFetchingResponse: boolean;
tableProcessedDataRef: React.MutableRefObject<RowData[]>;
setSearchTerm: React.Dispatch<React.SetStateAction<string>>;
}
function WidgetHeader({
@@ -67,6 +70,7 @@ function WidgetHeader({
isWarning,
isFetchingResponse,
tableProcessedDataRef,
setSearchTerm,
}: IWidgetHeaderProps): JSX.Element | null {
const onEditHandler = useCallback((): void => {
const widgetId = widget.id;
@@ -187,6 +191,10 @@ function WidgetHeader({
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
const [showGlobalSearch, setShowGlobalSearch] = useState(false);
const globalSearchAvailable = widget.panelTypes === PANEL_TYPES.TABLE;
const menu = useMemo(
() => ({
items: updatedMenuList,
@@ -201,46 +209,80 @@ function WidgetHeader({
return (
<div className="widget-header-container">
<Typography.Text
ellipsis
data-testid={title}
className="widget-header-title"
>
{title}
</Typography.Text>
<div className="widget-header-actions">
<div className="widget-api-actions">{threshold}</div>
{isFetchingResponse && !queryResponse.isError && (
<Spinner style={{ paddingRight: '0.25rem' }} />
)}
{queryResponse.isError && (
<Tooltip
title={errorMessage}
placement={errorTooltipPosition}
className="widget-api-actions"
{showGlobalSearch ? (
<Input
addonBefore={<SearchOutlined size={14} />}
placeholder="Search..."
bordered={false}
data-testid="widget-header-search-input"
autoFocus
addonAfter={
<X
size={14}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setShowGlobalSearch(false);
}}
className="search-header-icons"
/>
}
key={widget.id}
onChange={(e): void => {
setSearchTerm(e.target.value || '');
}}
/>
) : (
<>
<Typography.Text
ellipsis
data-testid={title}
className="widget-header-title"
>
<ExclamationCircleOutlined />
</Tooltip>
)}
{title}
</Typography.Text>
<div className="widget-header-actions">
<div className="widget-api-actions">{threshold}</div>
{isFetchingResponse && !queryResponse.isError && (
<Spinner style={{ paddingRight: '0.25rem' }} />
)}
{queryResponse.isError && (
<Tooltip
title={errorMessage}
placement={errorTooltipPosition}
className="widget-api-actions"
>
<ExclamationCircleOutlined />
</Tooltip>
)}
{isWarning && (
<Tooltip
title={WARNING_MESSAGE}
placement={errorTooltipPosition}
className="widget-api-actions"
>
<WarningOutlined />
</Tooltip>
)}
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<MoreOutlined
data-testid="widget-header-options"
className={`widget-header-more-options ${
parentHover ? 'widget-header-hover' : ''
}`}
/>
</Dropdown>
</div>
{isWarning && (
<Tooltip
title={WARNING_MESSAGE}
placement={errorTooltipPosition}
className="widget-api-actions"
>
<WarningOutlined />
</Tooltip>
)}
{globalSearchAvailable && (
<SearchOutlined
className="search-header-icons"
onClick={(): void => setShowGlobalSearch(true)}
data-testid="widget-header-search"
/>
)}
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<MoreOutlined
data-testid="widget-header-options"
className={`widget-header-more-options ${
parentHover ? 'widget-header-hover' : ''
} ${globalSearchAvailable ? 'widget-header-more-options-visible' : ''}`}
/>
</Dropdown>
</div>
</>
)}
</div>
);
}

View File

@@ -33,7 +33,14 @@ export const Card = styled(CardComponent)<CardProps>`
}
.ant-card-body {
height: calc(100% - 30px);
${({ $panelType }): StyledCSS =>
$panelType === PANEL_TYPES.TABLE
? css`
height: 100%;
`
: css`
height: calc(100% - 30px);
`}
padding: 0;
}
`;

View File

@@ -0,0 +1,5 @@
.long-text-tooltip {
max-width: 500px;
max-height: 500px;
overflow-y: auto;
}

View File

@@ -1,10 +1,13 @@
import './GridTableComponent.styles.scss';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Space, Tooltip } from 'antd';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Events } from 'constants/events';
import { QueryTable } from 'container/QueryTable';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { cloneDeep, get, isEmpty, set } from 'lodash-es';
import { cloneDeep, get, isEmpty } from 'lodash-es';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { eventEmitter } from 'utils/getEventEmitter';
@@ -38,15 +41,13 @@ function GridTableComponent({
const createDataInCorrectFormat = useCallback(
(dataSource: RowData[]): RowData[] =>
dataSource.map((d) => {
const finalObject = {};
const finalObject: Record<string, number | string> = {};
// we use the order of the columns here to have similar download as the user view
// the [] access for the object is used because the titles can contain dot(.) as well
columns.forEach((k) => {
set(
finalObject,
get(k, 'title', '') as string,
get(d, get(k, 'dataIndex', ''), 'n/a'),
);
finalObject[`${get(k, 'title', '')}`] =
d[`${get(k, 'dataIndex', '')}`] || 'n/a';
});
return finalObject as RowData;
}),
@@ -86,6 +87,7 @@ function GridTableComponent({
applyColumnUnits,
originalDataSource,
]);
useEffect(() => {
if (tableProcessedDataRef) {
// eslint-disable-next-line no-param-reassign
@@ -117,7 +119,16 @@ function GridTableComponent({
}
>
<Space>
{text}
<LineClampedText
text={text}
lines={3}
tooltipProps={{
placement: 'right',
autoAdjustOverflow: true,
overlayClassName: 'long-text-tooltip',
}}
/>
{hasMultipleMatches && (
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
<ExclamationCircleFilled className="value-graph-icon" />
@@ -128,7 +139,19 @@ function GridTableComponent({
);
}
}
return <div>{text}</div>;
return (
<div>
<LineClampedText
text={text}
lines={3}
tooltipProps={{
placement: 'right',
autoAdjustOverflow: true,
overlayClassName: 'long-text-tooltip',
}}
/>
</div>
);
},
}));

View File

@@ -14,6 +14,7 @@ export type GridTableComponentProps = {
columnUnits?: ColumnUnit;
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
sticky?: TableProps<RowData>['sticky'];
searchTerm?: string;
} & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;

View File

@@ -64,9 +64,9 @@
.dashboard-icon {
display: inline-block;
margin-top: 4px;
margin-right: 4px;
line-height: 20px;
height: 14px;
width: 14px;
}
.dot {
@@ -75,6 +75,12 @@
border-radius: 50%;
}
.title-link {
display: flex;
align-items: center;
gap: 8px;
}
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);

View File

@@ -459,17 +459,19 @@ function DashboardsList(): JSX.Element {
placement="left"
overlayClassName="title-toolip"
>
<Typography.Text data-testid={`dashboard-title-${index}`}>
<Link to={getLink()} className="title">
<img
src={dashboard?.image || Base64Icons[0]}
style={{ height: '14px', width: '14px' }}
alt="dashboard-image"
className="dashboard-icon"
/>
<Link to={getLink()} className="title-link">
<img
src={dashboard?.image || Base64Icons[0]}
alt="dashboard-image"
className="dashboard-icon"
/>
<Typography.Text
data-testid={`dashboard-title-${index}`}
className="title"
>
{dashboard.name}
</Link>
</Typography.Text>
</Typography.Text>
</Link>
</Tooltip>
</div>

View File

@@ -0,0 +1,34 @@
.empty-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.infra-metrics-container {
.views-tabs {
margin-bottom: 1rem;
}
}
.infra-metrics-card {
margin: 1rem 0;
height: 300px;
padding: 10px;
.ant-card-body {
padding: 0;
}
.chart-container {
width: 100%;
height: 100%;
}
.no-data-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

View File

@@ -0,0 +1,94 @@
import './InfraMetrics.styles.scss';
import { Empty, Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import { History, Table } from 'lucide-react';
import { useState } from 'react';
import { VIEW_TYPES } from './constants';
import NodeMetrics from './NodeMetrics';
import PodMetrics from './PodMetrics';
interface MetricsDataProps {
podName: string;
nodeName: string;
hostName: string;
clusterName: string;
logLineTimestamp: string;
}
function InfraMetrics({
podName,
nodeName,
hostName,
clusterName,
logLineTimestamp,
}: MetricsDataProps): JSX.Element {
const [selectedView, setSelectedView] = useState<string>(() =>
podName ? VIEW_TYPES.POD : VIEW_TYPES.NODE,
);
const handleModeChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
};
if (!podName && !nodeName && !hostName) {
return (
<div className="empty-container">
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="No data available. Please select a valid log line containing a pod, node, or host attributes to view metrics."
/>
</div>
);
}
return (
<div className="infra-metrics-container">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={selectedView === VIEW_TYPES.NODE ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.NODE}
>
<div className="view-title">
<Table size={14} />
Node
</div>
</Radio.Button>
{podName && (
<Radio.Button
className={selectedView === VIEW_TYPES.POD ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.POD}
>
<div className="view-title">
<History size={14} />
Pod
</div>
</Radio.Button>
)}
</Radio.Group>
{/* TODO(Rahul): Make a common config driven component for this and other infra metrics components */}
{selectedView === VIEW_TYPES.NODE && (
<NodeMetrics
nodeName={nodeName}
clusterName={clusterName}
hostName={hostName}
logLineTimestamp={logLineTimestamp}
/>
)}
{selectedView === VIEW_TYPES.POD && podName && (
<PodMetrics
podName={podName}
clusterName={clusterName}
logLineTimestamp={logLineTimestamp}
/>
)}
</div>
);
}
export default InfraMetrics;

View File

@@ -0,0 +1,140 @@
import { Card, Col, Row, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import Uplot from 'components/Uplot';
import { ENTITY_VERSION_V4 } from 'constants/app';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import {
getHostQueryPayload,
getNodeQueryPayload,
hostWidgetInfo,
nodeWidgetInfo,
} from './constants';
function NodeMetrics({
nodeName,
clusterName,
hostName,
logLineTimestamp,
}: {
nodeName: string;
clusterName: string;
hostName: string;
logLineTimestamp: string;
}): JSX.Element {
const { start, end, verticalLineTimestamp } = useMemo(() => {
const logTimestamp = dayjs(logLineTimestamp);
const now = dayjs();
const startTime = logTimestamp.subtract(3, 'hour');
const endTime = logTimestamp.add(3, 'hour').isBefore(now)
? logTimestamp.add(3, 'hour')
: now;
return {
start: startTime.unix(),
end: endTime.unix(),
verticalLineTimestamp: logTimestamp.unix(),
};
}, [logLineTimestamp]);
const queryPayloads = useMemo(() => {
if (nodeName) {
return getNodeQueryPayload(clusterName, nodeName, start, end);
}
return getHostQueryPayload(hostName, start, end);
}, [nodeName, hostName, clusterName, start, end]);
const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo;
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'NODE'],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const chartData = useMemo(
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
[queries],
);
const options = useMemo(
() =>
queries.map(({ data }, idx) =>
getUPlotChartOptions({
apiResponse: data?.payload,
isDarkMode,
dimensions,
yAxisUnit: widgetInfo[idx].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: start,
maxTimeScale: end,
verticalLineTimestamp,
}),
),
[
queries,
isDarkMode,
dimensions,
widgetInfo,
start,
verticalLineTimestamp,
end,
],
);
const renderCardContent = (
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
idx: number,
): JSX.Element => {
if (query.isLoading) {
return <Skeleton />;
}
if (query.error) {
const errorMessage =
(query.error as Error)?.message || 'Something went wrong';
return <div>{errorMessage}</div>;
}
return (
<div
className={cx('chart-container', {
'no-data-container':
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
>
<Uplot options={options[idx]} data={chartData[idx]} />
</div>
);
};
return (
<Row gutter={24}>
{queries.map((query, idx) => (
<Col span={12} key={widgetInfo[idx].title}>
<Typography.Text>{widgetInfo[idx].title}</Typography.Text>
<Card bordered className="infra-metrics-card" ref={graphRef}>
{renderCardContent(query, idx)}
</Card>
</Col>
))}
</Row>
);
}
export default NodeMetrics;

View File

@@ -0,0 +1,121 @@
import { Card, Col, Row, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import Uplot from 'components/Uplot';
import { ENTITY_VERSION_V4 } from 'constants/app';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { getPodQueryPayload, podWidgetInfo } from './constants';
function PodMetrics({
podName,
clusterName,
logLineTimestamp,
}: {
podName: string;
clusterName: string;
logLineTimestamp: string;
}): JSX.Element {
const { start, end, verticalLineTimestamp } = useMemo(() => {
const logTimestamp = dayjs(logLineTimestamp);
const now = dayjs();
const startTime = logTimestamp.subtract(3, 'hour');
const endTime = logTimestamp.add(3, 'hour').isBefore(now)
? logTimestamp.add(3, 'hour')
: now;
return {
start: startTime.unix(),
end: endTime.unix(),
verticalLineTimestamp: logTimestamp.unix(),
};
}, [logLineTimestamp]);
const queryPayloads = useMemo(
() => getPodQueryPayload(clusterName, podName, start, end),
[clusterName, end, podName, start],
);
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'POD'],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const chartData = useMemo(
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
[queries],
);
const options = useMemo(
() =>
queries.map(({ data }, idx) =>
getUPlotChartOptions({
apiResponse: data?.payload,
isDarkMode,
dimensions,
yAxisUnit: podWidgetInfo[idx].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: start,
maxTimeScale: end,
verticalLineTimestamp,
}),
),
[queries, isDarkMode, dimensions, start, verticalLineTimestamp, end],
);
const renderCardContent = (
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
idx: number,
): JSX.Element => {
if (query.isLoading) {
return <Skeleton />;
}
if (query.error) {
const errorMessage =
(query.error as Error)?.message || 'Something went wrong';
return <div>{errorMessage}</div>;
}
return (
<div
className={cx('chart-container', {
'no-data-container':
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
>
<Uplot options={options[idx]} data={chartData[idx]} />
</div>
);
};
return (
<Row gutter={24}>
{queries.map((query, idx) => (
<Col span={12} key={podWidgetInfo[idx].title}>
<Typography.Text>{podWidgetInfo[idx].title}</Typography.Text>
<Card bordered className="infra-metrics-card" ref={graphRef}>
{renderCardContent(query, idx)}
</Card>
</Col>
))}
</Row>
);
}
export default PodMetrics;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import './LogsExplorerQuerySection.styles.scss';
import { FeatureKeys } from 'constants/features';
import {
initialQueriesMap,
OPERATORS,
@@ -9,14 +8,12 @@ import {
import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useFeatureFlags from 'hooks/useFeatureFlag';
import {
prepareQueryWithDefaultTimestamp,
SELECTED_VIEWS,
@@ -89,26 +86,15 @@ function LogExplorerQuerySection({
[handleChangeQueryData],
);
const isSearchV2Enabled =
useFeatureFlags(FeatureKeys.QUERY_BUILDER_SEARCH_V2)?.active || false;
return (
<>
{selectedView === SELECTED_VIEWS.SEARCH && (
<div className="qb-search-view-container">
{isSearchV2Enabled ? (
<QueryBuilderSearchV2
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
) : (
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
)}
<QueryBuilderSearchV2
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
</div>
)}

View File

@@ -9,15 +9,18 @@ export function getColorsForSeverityLabels(
const lowerCaseLabel = label.toLowerCase();
if (lowerCaseLabel.includes(`{severity_text="trace"}`)) {
return Color.BG_ROBIN_300;
return Color.BG_FOREST_400;
}
if (lowerCaseLabel.includes(`{severity_text="debug"}`)) {
return Color.BG_FOREST_500;
return Color.BG_AQUA_500;
}
if (lowerCaseLabel.includes(`{severity_text="info"}`)) {
return Color.BG_SLATE_400;
if (
lowerCaseLabel.includes(`{severity_text="info"}`) ||
lowerCaseLabel.includes(`{severity_text=""}`)
) {
return Color.BG_ROBIN_500;
}
if (lowerCaseLabel.includes(`{severity_text="warn"}`)) {

View File

@@ -28,6 +28,20 @@ const lodsQueryServerRequest = (): void =>
),
);
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// mocking the graph components in this test as this should be handled separately
jest.mock(
'container/TimeSeriesView/TimeSeriesView',

View File

@@ -63,6 +63,7 @@
height: 40px;
justify-content: end;
padding: 0 8px;
margin: 12px 0 2px;
}
}

View File

@@ -130,12 +130,16 @@
.left-section {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
width: 45%;
.dashboard-img {
height: 16px;
width: 16px;
}
.dashboard-title {
color: #fff;
font-family: Inter;

View File

@@ -306,16 +306,13 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
</div>
<section className="dashboard-details">
<div className="left-section">
<img src={image} alt="dashboard-img" className="dashboard-img" />
<Tooltip title={title.length > 30 ? title : ''}>
<Typography.Text
className="dashboard-title"
data-testid="dashboard-title"
>
<img
src={image}
alt="dashboard-img"
style={{ width: '16px', height: '16px' }}
/>{' '}
{' '}
{title}
</Typography.Text>
</Tooltip>

View File

@@ -43,6 +43,15 @@
.ant-select-item {
display: flex;
align-items: center;
gap: 8px;
}
.rc-virtual-list-holder {
[data-testid='option-ALL'] {
border-bottom: 1px solid var(--bg-slate-400);
padding-bottom: 12px;
margin-bottom: 8px;
}
}
.all-label {
@@ -56,28 +65,25 @@
}
.dropdown-value {
display: flex;
justify-content: space-between;
align-items: center;
display: grid;
grid-template-columns: 1fr max-content;
.option-text {
max-width: 180px;
padding: 0 8px;
}
.toggle-tag-label {
padding-left: 8px;
right: 40px;
font-weight: normal;
position: absolute;
font-weight: 500;
}
}
}
}
.dropdown-styles {
min-width: 300px;
max-width: 350px;
min-width: 400px;
max-width: 500px;
}
.lightMode {

View File

@@ -1,14 +1,8 @@
import '@testing-library/jest-dom/extend-expect';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import React, { useEffect } from 'react';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import VariableItem from './VariableItem';

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -25,8 +26,11 @@ import { debounce, isArray, isString } from 'lodash-es';
import map from 'lodash-es/map';
import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
import { GlobalReducer } from 'types/reducer/globalTime';
import { popupContainer } from 'utils/selectPopupContainer';
import { variablePropsToPayloadVariables } from '../utils';
@@ -58,14 +62,14 @@ interface VariableItemProps {
const getSelectValue = (
selectedValue: IDashboardVariable['selectedValue'],
variableData: IDashboardVariable,
): string | string[] => {
): string | string[] | undefined => {
if (Array.isArray(selectedValue)) {
if (!variableData.multiSelect && selectedValue.length === 1) {
return selectedValue[0]?.toString() || '';
return selectedValue[0]?.toString();
}
return selectedValue.map((item) => item.toString());
}
return selectedValue?.toString() || '';
return selectedValue?.toString();
};
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -80,6 +84,23 @@ function VariableItem({
[],
);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
useEffect(() => {
if (variableData.allSelected && variableData.type === 'QUERY') {
setVariablesToGetUpdated((prev) => {
const variablesQueue = [...prev.filter((v) => v !== variableData.name)];
if (variableData.name) {
variablesQueue.push(variableData.name);
}
return variablesQueue;
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime]);
const [errorMessage, setErrorMessage] = useState<null | string>(null);
const getDependentVariables = (queryValue: string): string[] => {
@@ -111,7 +132,14 @@ function VariableItem({
const variableKey = dependentVariablesStr.replace(/\s/g, '');
return [REACT_QUERY_KEY.DASHBOARD_BY_ID, variableName, variableKey];
// added this time dependency for variables query as API respects the passed time range now
return [
REACT_QUERY_KEY.DASHBOARD_BY_ID,
variableName,
variableKey,
`${minTime}`,
`${maxTime}`,
];
};
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -151,10 +179,14 @@ function VariableItem({
valueNotInList = true;
}
}
// variablesData.allSelected is added for the case where on change of options we need to update the
// local storage
if (
variableData.type === 'QUERY' &&
variableData.name &&
(variablesToGetUpdated.includes(variableData.name) || valueNotInList)
(variablesToGetUpdated.includes(variableData.name) ||
valueNotInList ||
variableData.allSelected)
) {
let value = variableData.selectedValue;
let allSelected = false;
@@ -268,7 +300,7 @@ function VariableItem({
e.stopPropagation();
e.preventDefault();
const isChecked =
variableData.allSelected || selectValue.includes(ALL_SELECT_VALUE);
variableData.allSelected || selectValue?.includes(ALL_SELECT_VALUE);
if (isChecked) {
handleChange([]);
@@ -338,8 +370,8 @@ function VariableItem({
(Array.isArray(selectValue) && selectValue?.includes(option.toString()));
if (isChecked) {
if (mode === ToggleTagValue.Only) {
handleChange(option.toString());
if (mode === ToggleTagValue.Only && variableData.multiSelect) {
handleChange([option.toString()]);
} else if (!variableData.multiSelect) {
handleChange(option.toString());
} else {
@@ -430,6 +462,7 @@ function VariableItem({
<span>+ {omittedValues.length} </span>
</Tooltip>
)}
allowClear
>
{enableSelectAll && (
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
@@ -468,11 +501,17 @@ function VariableItem({
{...retProps(option as string)}
onClick={(e): void => handleToggle(e as any, option as string)}
>
<Tooltip title={option.toString()} placement="bottomRight">
<Typography.Text ellipsis className="option-text">
{option.toString()}
</Typography.Text>
</Tooltip>
<Typography.Text
ellipsis={{
tooltip: {
placement: variableData.multiSelect ? 'top' : 'right',
autoAdjustOverflow: true,
},
}}
className="option-text"
>
{option.toString()}
</Typography.Text>
{variableData.multiSelect &&
optionState.tag === option.toString() &&

View File

@@ -16,6 +16,7 @@ function PanelWrapper({
selectedGraph,
tableProcessedDataRef,
customTooltipElement,
searchTerm,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
selectedGraph || widget.panelTypes
@@ -39,6 +40,7 @@ function PanelWrapper({
selectedGraph={selectedGraph}
tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
searchTerm={searchTerm}
/>
);
}

View File

@@ -8,6 +8,7 @@ function TablePanelWrapper({
widget,
queryResponse,
tableProcessedDataRef,
searchTerm,
}: PanelWrapperProps): JSX.Element {
const panelData =
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
@@ -20,6 +21,7 @@ function TablePanelWrapper({
columnUnits={widget.columnUnits}
tableProcessedDataRef={tableProcessedDataRef}
sticky={widget.panelTypes === PANEL_TYPES.TABLE}
searchTerm={searchTerm}
// eslint-disable-next-line react/jsx-props-no-spreading
{...GRID_TABLE_CONFIG}
/>

View File

@@ -266,14 +266,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
class="ant-table-cell"
>
<div>
demo-app
<div
class="line-clamped-text"
>
demo-app
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
4.35 s
<div
class="line-clamped-text"
>
4.35 s
</div>
</div>
</td>
</tr>
@@ -284,14 +292,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
class="ant-table-cell"
>
<div>
customer
<div
class="line-clamped-text"
>
customer
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
431 ms
<div
class="line-clamped-text"
>
431 ms
</div>
</div>
</td>
</tr>
@@ -302,14 +318,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
class="ant-table-cell"
>
<div>
mysql
<div
class="line-clamped-text"
>
mysql
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
431 ms
<div
class="line-clamped-text"
>
431 ms
</div>
</div>
</td>
</tr>
@@ -320,14 +344,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
class="ant-table-cell"
>
<div>
frontend
<div
class="line-clamped-text"
>
frontend
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
287 ms
<div
class="line-clamped-text"
>
287 ms
</div>
</div>
</td>
</tr>
@@ -338,14 +370,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
class="ant-table-cell"
>
<div>
driver
<div
class="line-clamped-text"
>
driver
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
230 ms
<div
class="line-clamped-text"
>
230 ms
</div>
</div>
</td>
</tr>
@@ -356,14 +396,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
class="ant-table-cell"
>
<div>
route
<div
class="line-clamped-text"
>
route
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
66.4 ms
<div
class="line-clamped-text"
>
66.4 ms
</div>
</div>
</td>
</tr>
@@ -374,14 +422,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
class="ant-table-cell"
>
<div>
redis
<div
class="line-clamped-text"
>
redis
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
31.3 ms
<div
class="line-clamped-text"
>
31.3 ms
</div>
</div>
</td>
</tr>

View File

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

View File

@@ -9,6 +9,20 @@ import store from 'store';
import ChangeHistory from '../index';
import { pipelineData, pipelineDataHistory } from './testUtils';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {

View File

@@ -9,6 +9,20 @@ import store from 'store';
import { pipelineMockData } from '../mocks/pipeline';
import AddNewPipeline from '../PipelineListsView/AddNewPipeline';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
export function matchMedia(): void {
Object.defineProperty(window, 'matchMedia', {
writable: true,

View File

@@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline';
import AddNewProcessor from '../PipelineListsView/AddNewProcessor';
import { matchMedia } from './AddNewPipeline.test';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
beforeAll(() => {
matchMedia();
});

View File

@@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => {
it('should render DeleteAction section', () => {
const { asFragment } = render(

View File

@@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => {
it('should render DragAction section', () => {
const { asFragment } = render(

View File

@@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => {
it('should render EditAction section', () => {
const { asFragment } = render(

View File

@@ -8,6 +8,20 @@ import store from 'store';
import { pipelineMockData } from '../mocks/pipeline';
import PipelineActions from '../PipelineListsView/TableComponents/PipelineActions';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => {
it('should render PipelineActions section', () => {
const { asFragment } = render(

View File

@@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline';
import PipelineExpandView from '../PipelineListsView/PipelineExpandView';
import { matchMedia } from './AddNewPipeline.test';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
beforeAll(() => {
matchMedia();
});

View File

@@ -11,6 +11,20 @@ import store from 'store';
import { pipelineApiResponseMockData } from '../mocks/pipeline';
import PipelineListsView from '../PipelineListsView';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
const samplePipelinePreviewResponse = {
isLoading: false,
logs: [

View File

@@ -11,6 +11,20 @@ import { v4 } from 'uuid';
import PipelinePageLayout from '../Layouts/Pipeline';
import { matchMedia } from './AddNewPipeline.test';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
beforeAll(() => {
matchMedia();
});

View File

@@ -7,6 +7,20 @@ import store from 'store';
import TagInput from '../components/TagInput';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('Pipeline Page', () => {
it('should render TagInput section', () => {
const { asFragment } = render(

View File

@@ -11,6 +11,20 @@ import {
getTableColumn,
} from '../PipelineListsView/utils';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('Utils testing of Pipeline Page', () => {
test('it should be check form field of add pipeline', () => {
expect(pipelineFields.length).toBe(3);

View File

@@ -23,6 +23,7 @@ import {
import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter';
import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
// ** Hooks
@@ -81,6 +82,10 @@ export const Query = memo(function Query({
entityVersion: version,
});
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
@@ -452,11 +457,19 @@ export const Query = memo(function Query({
</Col>
)}
<Col flex="1" className="qb-search-container">
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
{isLogsExplorerPage ? (
<QueryBuilderSearchV2
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
) : (
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
)}
</Col>
</Row>
</Col>

View File

@@ -1,3 +1,4 @@
import { Color } from '@signozhq/design-tokens';
import { Select } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
// ** Constants
@@ -34,6 +35,7 @@ export function HavingFilter({
const [currentFormValue, setCurrentFormValue] = useState<HavingForm>(
initialHavingValues,
);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { isMulti } = useTagValidation(
currentFormValue.op,
@@ -198,6 +200,29 @@ export function HavingFilter({
resetChanges();
};
const handleFocus = useCallback(() => {
setErrorMessage(null);
}, []);
const handleBlur = useCallback((): void => {
if (searchText) {
const { columnName, op, value } = getHavingObject(searchText);
const isCompleteHavingClause =
columnName && op && value.every((v) => v !== '');
if (isCompleteHavingClause && isValidHavingValue(searchText)) {
setLocalValues((prev) => {
const updatedValues = [...prev, searchText];
onChange(updatedValues.map(transformFromStringToHaving));
return updatedValues;
});
setSearchText('');
} else {
setErrorMessage('Invalid HAVING clause');
}
}
}, [searchText, onChange]);
useEffect(() => {
parseSearchText(searchText);
}, [searchText, parseSearchText]);
@@ -209,28 +234,36 @@ export function HavingFilter({
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
return (
<Select
getPopupContainer={popupContainer}
autoClearSearchValue={false}
mode="multiple"
onSearch={handleSearch}
searchValue={searchText}
tagRender={tagRender}
value={localValues}
data-testid="havingSelect"
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={{ width: '100%' }}
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
placeholder="GroupBy(operation) > 5"
onDeselect={handleDeselect}
onChange={handleChange}
onSelect={handleSelect}
>
{options.map((opt) => (
<Select.Option key={opt.value} value={opt.value} title="havingOption">
{opt.label}
</Select.Option>
))}
</Select>
<>
<Select
getPopupContainer={popupContainer}
autoClearSearchValue={false}
mode="multiple"
onSearch={handleSearch}
searchValue={searchText}
tagRender={tagRender}
value={localValues}
data-testid="havingSelect"
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={{ width: '100%' }}
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
placeholder="GroupBy(operation) > 5"
onDeselect={handleDeselect}
onChange={handleChange}
onSelect={handleSelect}
onFocus={handleFocus}
onBlur={handleBlur}
status={errorMessage ? 'error' : undefined}
>
{options.map((opt) => (
<Select.Option key={opt.value} value={opt.value} title="havingOption">
{opt.label}
</Select.Option>
))}
</Select>
{errorMessage && (
<div style={{ color: Color.BG_CHERRY_500 }}>{errorMessage}</div>
)}
</>
);
}

View File

@@ -2,6 +2,10 @@
display: flex;
gap: 4px;
.ant-select-dropdown {
padding: 0px;
}
.show-all-filters {
.content {
.rc-virtual-list-holder {
@@ -231,16 +235,16 @@
}
&.resource {
border: 1px solid rgba(242, 71, 105, 0.2);
border: 1px solid #4bcff920;
.ant-typography {
color: var(--bg-sakura-400);
background: rgba(245, 108, 135, 0.1);
color: var(--bg-aqua-400);
background: #4bcff910;
font-size: 14px;
}
.ant-tag-close-icon {
background: rgba(245, 108, 135, 0.1);
background: #4bcff910;
}
}
&.tag {
@@ -259,3 +263,110 @@
}
}
}
.lightMode {
.query-builder-search-v2 {
.content {
.operator-for {
.operator-for-text {
color: var(--bg-ink-200);
}
.operator-for-value {
background: rgba(255, 255, 255, 0.1);
color: var(--bg-ink-200);
}
}
.value-for {
.value-for-text {
color: var(--bg-ink-200);
}
.value-for-value {
background: rgba(255, 255, 255, 0.1);
color: var(--bg-ink-400);
}
}
.example-queries {
cursor: default;
.heading {
color: var(--bg-slate-50);
}
.query-container {
.example-query {
background: var(--bg-vanilla-200);
color: var(--bg-ink-400);
}
.example-query:hover {
color: var(--bg-ink-100);
}
}
}
}
.keyboard-shortcuts {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-200);
.icons {
border-top: 1.143px solid var(--bg-ink-200);
border-right: 1.143px solid var(--bg-ink-200);
border-bottom: 2.286px solid var(--bg-ink-200);
border-left: 1.143px solid var(--bg-ink-200);
background: var(--bg-vanilla-300);
}
.keyboard-text {
color: var(--bg-ink-400);
}
.navigate {
border-right: 1px solid #1d212d;
}
.show-all-filter-items {
border-left: 1px solid #1d212d;
}
}
.qb-search-bar-tokenised-tags {
.ant-tag {
border: 1px solid var(--bg-slate-100);
background: var(--bg-vanilla-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
.ant-typography {
color: var(--bg-ink-100);
}
&.resource {
border: 1px solid #4bcff920;
.ant-typography {
color: var(--bg-aqua-400);
background: #4bcff910;
}
.ant-tag-close-icon {
background: #4bcff910;
}
}
&.tag {
border: 1px solid rgba(189, 153, 121, 0.2);
.ant-typography {
color: var(--bg-sienna-400);
background: rgba(189, 153, 121, 0.1);
}
.ant-tag-close-icon {
background: rgba(189, 153, 121, 0.1);
}
}
}
}
}
}

View File

@@ -286,16 +286,62 @@ function QueryBuilderSearchV2(
parsedValue = value;
}
if (currentState === DropdownState.ATTRIBUTE_KEY) {
setCurrentFilterItem((prev) => ({
...prev,
key: parsedValue as BaseAutocompleteData,
op: '',
value: '',
}));
setCurrentState(DropdownState.OPERATOR);
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
// Case - convert abc def ghi type attribute keys directly to body contains abc def ghi
if (
isObject(parsedValue) &&
parsedValue?.key &&
parsedValue?.key?.split(' ').length > 1
) {
setTags((prev) => [
...prev,
{
key: {
key: 'body',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
// eslint-disable-next-line sonarjs/no-duplicate-string
id: 'body--string----true',
},
op: OPERATORS.CONTAINS,
value: (parsedValue as BaseAutocompleteData)?.key,
},
]);
setCurrentFilterItem(undefined);
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else {
setCurrentFilterItem((prev) => ({
...prev,
key: parsedValue as BaseAutocompleteData,
op: '',
value: '',
}));
setCurrentState(DropdownState.OPERATOR);
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
}
} else if (currentState === DropdownState.OPERATOR) {
if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) {
if (isEmpty(value) && currentFilterItem?.key?.key) {
setTags((prev) => [
...prev,
{
key: {
key: 'body',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'body--string----true',
},
op: OPERATORS.CONTAINS,
value: currentFilterItem?.key?.key,
},
]);
setCurrentFilterItem(undefined);
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) {
setTags((prev) => [
...prev,
{
@@ -399,6 +445,7 @@ function QueryBuilderSearchV2(
whereClauseConfig?.customKey === 'body' &&
whereClauseConfig?.customOp === OPERATORS.CONTAINS
) {
// eslint-disable-next-line sonarjs/no-identical-functions
setTags((prev) => [
...prev,
{
@@ -519,19 +566,20 @@ function QueryBuilderSearchV2(
setCurrentState(DropdownState.OPERATOR);
}
}
if (suggestionsData?.payload?.attributes?.length === 0) {
// again let's not auto select anything for the user
if (tagOperator) {
setCurrentFilterItem({
key: {
key: tagKey.split(' ')[0],
key: tagKey,
dataType: DataTypes.EMPTY,
type: '',
isColumn: false,
isJSON: false,
},
op: '',
op: tagOperator,
value: '',
});
setCurrentState(DropdownState.OPERATOR);
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
}
} else if (
// Case 2 - if key is defined but the search text doesn't match with the set key,
@@ -607,13 +655,32 @@ function QueryBuilderSearchV2(
// the useEffect takes care of setting the dropdown values correctly on change of the current state
useEffect(() => {
if (currentState === DropdownState.ATTRIBUTE_KEY) {
const { tagKey } = getTagToken(searchValue);
if (isLogsExplorerPage) {
setDropdownOptions(
suggestionsData?.payload?.attributes?.map((key) => ({
// add the user typed option in the dropdown to select that and move ahead irrespective of the matches and all
setDropdownOptions([
...(!isEmpty(tagKey) &&
!suggestionsData?.payload?.attributes?.some((val) =>
isEqual(val.key, tagKey),
)
? [
{
label: tagKey,
value: {
key: tagKey,
dataType: DataTypes.EMPTY,
type: '',
isColumn: false,
isJSON: false,
},
},
]
: []),
...(suggestionsData?.payload?.attributes?.map((key) => ({
label: key.key,
value: key,
})) || [],
);
})) || []),
]);
} else {
setDropdownOptions(
data?.payload?.attributeKeys?.map((key) => ({
@@ -643,12 +710,14 @@ function QueryBuilderSearchV2(
op.label.startsWith(partialOperator.toLocaleUpperCase()),
);
}
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
label: operator,
value: operator,
}));
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
} else {
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
@@ -663,6 +732,7 @@ function QueryBuilderSearchV2(
op.label.startsWith(partialOperator.toLocaleUpperCase()),
);
}
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
setDropdownOptions(operatorOptions);
}
}
@@ -729,7 +799,8 @@ function QueryBuilderSearchV2(
}, [tags]);
useEffect(() => {
if (!isEqual(query.filters.items, tags)) {
// convert the query and tags to same format before comparison
if (!isEqual(getInitTags(query), tags)) {
setTags(getInitTags(query));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -769,7 +840,7 @@ function QueryBuilderSearchV2(
);
const queryTags = useMemo(
() => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`),
() => tags.map((tag) => `${tag.key?.key} ${tag.op} ${tag.value}`),
[tags],
);

View File

@@ -77,14 +77,14 @@
&.resource {
border-radius: 50px;
background: rgba(245, 108, 135, 0.1) !important;
color: var(--bg-sakura-400) !important;
background: #4bcff910 !important;
color: var(--bg-aqua-400) !important;
.dot {
background-color: var(--bg-sakura-400);
background-color: var(--bg-aqua-400);
}
.text {
color: var(--bg-sakura-400);
color: var(--bg-aqua-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
@@ -168,3 +168,59 @@
}
}
}
.lightMode {
.text {
color: var(--bg-ink-400);
}
.option {
.container {
display: flex;
align-items: center;
justify-content: space-between;
.right-section {
.data-type {
background: var(--bg-vanilla-300);
}
}
.option-meta-data-container {
display: flex;
gap: 8px;
}
}
.container-without-tag {
.left {
.OPERATOR {
color: var(--bg-ink-400);
}
.VALUE {
color: var(--bg-ink-400);
}
}
.right {
.data-type {
background: var(--bg-vanilla-300);
}
}
}
}
.option:hover {
.container {
.left-section {
.value {
color: var(--bg-ink-100);
}
}
}
.container-without-tag {
.value {
color: var(--bg-ink-100);
}
}
}
}

View File

@@ -19,4 +19,5 @@ export type QueryTableProps = Omit<
columns?: ColumnsType<RowData>;
dataSource?: RowData[];
sticky?: TableProps<RowData>['sticky'];
searchTerm?: string;
};

View File

@@ -3,8 +3,11 @@ import './QueryTable.styles.scss';
import { ResizeTable } from 'components/ResizeTable';
import Download from 'container/Download/Download';
import { IServiceName } from 'container/MetricsApplication/Tabs/types';
import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
import { useMemo } from 'react';
import {
createTableColumnsFromQuery,
RowData,
} from 'lib/query/createTableColumnsFromQuery';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { QueryTableProps } from './QueryTable.intefaces';
@@ -20,6 +23,7 @@ export function QueryTable({
columns,
dataSource,
sticky,
searchTerm,
...props
}: QueryTableProps): JSX.Element {
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
@@ -55,6 +59,27 @@ export function QueryTable({
hideOnSinglePage: true,
};
const [filterTable, setFilterTable] = useState<RowData[] | null>(null);
const onTableSearch = useCallback(
(value?: string): void => {
const filterTable = newDataSource.filter((o) =>
Object.keys(o).some((k) =>
String(o[k])
.toLowerCase()
.includes(value?.toLowerCase() || ''),
),
);
setFilterTable(filterTable);
},
[newDataSource],
);
useEffect(() => {
onTableSearch(searchTerm);
}, [newDataSource, onTableSearch, searchTerm]);
return (
<div className="query-table">
{isDownloadEnabled && (
@@ -69,7 +94,7 @@ export function QueryTable({
<ResizeTable
columns={tableColumns}
tableLayout="fixed"
dataSource={newDataSource}
dataSource={filterTable === null ? newDataSource : filterTable}
scroll={{ x: true }}
pagination={paginationConfig}
sticky={sticky}

View File

@@ -0,0 +1,73 @@
/* eslint-disable react/jsx-props-no-spreading */
import WidgetHeader from 'container/GridCardLayout/WidgetHeader';
import { fireEvent, render } from 'tests/test-utils';
import { QueryTable } from '../QueryTable';
import { QueryTableProps, WidgetHeaderProps } from './mocks';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: ``,
}),
}));
// Mock useDashabord hook
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): any => ({
selectedDashboard: {
data: {
variables: [],
},
},
}),
}));
describe('QueryTable -', () => {
it('should render correctly with all the data rows', () => {
const { container } = render(<QueryTable {...QueryTableProps} />);
const tableRows = container.querySelectorAll('tr.ant-table-row');
expect(tableRows.length).toBe(QueryTableProps.queryTableData.rows.length);
});
it('should render correctly with searchTerm', () => {
const { container } = render(
<QueryTable {...QueryTableProps} searchTerm="frontend" />,
);
const tableRows = container.querySelectorAll('tr.ant-table-row');
expect(tableRows.length).toBe(3);
});
});
const setSearchTerm = jest.fn();
describe('WidgetHeader -', () => {
it('global search option should be working', () => {
const { getByText, getByTestId } = render(
<WidgetHeader {...WidgetHeaderProps} setSearchTerm={setSearchTerm} />,
);
expect(getByText('Table - Panel')).toBeInTheDocument();
const searchWidget = getByTestId('widget-header-search');
expect(searchWidget).toBeInTheDocument();
// click and open the search input
fireEvent.click(searchWidget);
// check if input is opened
const searchInput = getByTestId('widget-header-search-input');
expect(searchInput).toBeInTheDocument();
// enter search term
fireEvent.change(searchInput, { target: { value: 'frontend' } });
// check if search term is set
expect(setSearchTerm).toHaveBeenCalledWith('frontend');
expect(searchInput).toHaveValue('frontend');
});
it('global search should not be present for non-table panel', () => {
const { queryByTestId } = render(
<WidgetHeader
{...WidgetHeaderProps}
widget={{ ...WidgetHeaderProps.widget, panelTypes: 'chart' }}
/>,
);
expect(queryByTestId('widget-header-search')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,797 @@
/* eslint-disable sonarjs/no-duplicate-string */
export const QueryTableProps: any = {
props: {
loading: false,
size: 'small',
},
queryTableData: {
columns: [
{
name: 'resource_host_name',
queryName: '',
isValueColumn: false,
},
{
name: 'service_name',
queryName: '',
isValueColumn: false,
},
{
name: 'operation',
queryName: '',
isValueColumn: false,
},
{
name: 'A',
queryName: 'A',
isValueColumn: true,
},
],
rows: [
{
data: {
A: 11.5,
operation: 'GetDriver',
resource_host_name: 'test-hs-name',
service_name: 'redis',
},
},
{
data: {
A: 10.13,
operation: 'HTTP GET',
resource_host_name: 'test-hs-name',
service_name: 'frontend',
},
},
{
data: {
A: 9.21,
operation: 'HTTP GET /route',
resource_host_name: 'test-hs-name',
service_name: 'route',
},
},
{
data: {
A: 9.21,
operation: 'HTTP GET: /route',
resource_host_name: 'test-hs-name',
service_name: 'frontend',
},
},
{
data: {
A: 0.92,
operation: 'HTTP GET: /customer',
resource_host_name: 'test-hs-name',
service_name: 'frontend',
},
},
{
data: {
A: 0.92,
operation: 'SQL SELECT',
resource_host_name: 'test-hs-name',
service_name: 'mysql',
},
},
{
data: {
A: 0.92,
operation: 'HTTP GET /customer',
resource_host_name: 'test-hs-name',
service_name: 'customer',
},
},
],
},
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_calls_total--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'signoz_calls_total',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: 'metrics',
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: 'string',
id: 'resource_host_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'resource_host_name',
type: 'tag',
},
{
dataType: 'string',
id: 'service_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
},
{
dataType: 'string',
id: 'operation--string--tag--false',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
},
],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '1e08128f-c6a3-42ff-8033-4e38d291cf0a',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: 'builder',
},
columns: [
{
dataIndex: 'resource_host_name',
title: 'resource_host_name',
width: 145,
},
{
dataIndex: 'service_name',
title: 'service_name',
width: 145,
},
{
dataIndex: 'operation',
title: 'operation',
width: 145,
},
{
dataIndex: 'A',
title: 'A',
width: 145,
},
],
dataSource: [
{
A: 11.5,
operation: 'GetDriver',
resource_host_name: 'test-hs-name',
service_name: 'redis',
},
{
A: 10.13,
operation: 'HTTP GET',
resource_host_name: 'test-hs-name',
service_name: 'frontend',
},
{
A: 9.21,
operation: 'HTTP GET /route',
resource_host_name: 'test-hs-name',
service_name: 'route',
},
{
A: 9.21,
operation: 'HTTP GET: /route',
resource_host_name: 'test-hs-name',
service_name: 'frontend',
},
{
A: 0.92,
operation: 'HTTP GET: /customer',
resource_host_name: 'test-hs-name',
service_name: 'frontend',
},
{
A: 0.92,
operation: 'SQL SELECT',
resource_host_name: 'test-hs-name',
service_name: 'mysql',
},
{
A: 0.92,
operation: 'HTTP GET /customer',
resource_host_name: 'test-hs-name',
service_name: 'customer',
},
],
sticky: true,
searchTerm: '',
};
export const WidgetHeaderProps: any = {
title: 'Table - Panel',
widget: {
bucketCount: 30,
bucketWidth: 0,
columnUnits: {},
description: '',
fillSpans: false,
id: 'add65f0d-7662-4024-af51-da567759235d',
isStacked: false,
mergeAllActiveQueries: false,
nullZeroValues: 'zero',
opacity: '1',
panelTypes: 'table',
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_calls_total--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'signoz_calls_total',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: 'metrics',
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: 'string',
id: 'resource_host_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'resource_host_name',
type: 'tag',
},
{
dataType: 'string',
id: 'service_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
},
{
dataType: 'string',
id: 'operation--string--tag--false',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
},
],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '1e08128f-c6a3-42ff-8033-4e38d291cf0a',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: 'builder',
},
selectedLogFields: [
{
dataType: 'string',
name: 'body',
type: '',
},
{
dataType: 'string',
name: 'timestamp',
type: '',
},
],
selectedTracesFields: [
{
dataType: 'string',
id: 'serviceName--string--tag--true',
isColumn: true,
isJSON: false,
key: 'serviceName',
type: 'tag',
},
{
dataType: 'string',
id: 'name--string--tag--true',
isColumn: true,
isJSON: false,
key: 'name',
type: 'tag',
},
{
dataType: 'float64',
id: 'durationNano--float64--tag--true',
isColumn: true,
isJSON: false,
key: 'durationNano',
type: 'tag',
},
{
dataType: 'string',
id: 'httpMethod--string--tag--true',
isColumn: true,
isJSON: false,
key: 'httpMethod',
type: 'tag',
},
{
dataType: 'string',
id: 'responseStatusCode--string--tag--true',
isColumn: true,
isJSON: false,
key: 'responseStatusCode',
type: 'tag',
},
],
softMax: 0,
softMin: 0,
stackedBarChart: false,
thresholds: [],
timePreferance: 'GLOBAL_TIME',
title: 'Table - Panel',
yAxisUnit: 'none',
},
parentHover: false,
queryResponse: {
status: 'success',
isLoading: false,
isSuccess: true,
isError: false,
isIdle: false,
data: {
statusCode: 200,
error: null,
message: 'success',
payload: {
status: 'success',
data: {
resultType: '',
result: [
{
table: {
columns: [
{
name: 'resource_host_name',
queryName: '',
isValueColumn: false,
},
{
name: 'service_name',
queryName: '',
isValueColumn: false,
},
{
name: 'operation',
queryName: '',
isValueColumn: false,
},
{
name: 'A',
queryName: 'A',
isValueColumn: true,
},
],
rows: [
{
data: {
A: 11.67,
operation: 'GetDriver',
resource_host_name: '4f6ec470feea',
service_name: 'redis',
},
},
{
data: {
A: 10.26,
operation: 'HTTP GET',
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
},
},
{
data: {
A: 9.33,
operation: 'HTTP GET: /route',
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
},
},
{
data: {
A: 9.33,
operation: 'HTTP GET /route',
resource_host_name: '4f6ec470feea',
service_name: 'route',
},
},
{
data: {
A: 0.93,
operation: 'FindDriverIDs',
resource_host_name: '4f6ec470feea',
service_name: 'redis',
},
},
{
data: {
A: 0.93,
operation: 'HTTP GET: /customer',
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
},
},
{
data: {
A: 0.93,
operation: '/driver.DriverService/FindNearest',
resource_host_name: '4f6ec470feea',
service_name: 'driver',
},
},
{
data: {
A: 0.93,
operation: '/driver.DriverService/FindNearest',
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
},
},
{
data: {
A: 0.93,
operation: 'SQL SELECT',
resource_host_name: '4f6ec470feea',
service_name: 'mysql',
},
},
{
data: {
A: 0.93,
operation: 'HTTP GET /customer',
resource_host_name: '4f6ec470feea',
service_name: 'customer',
},
},
{
data: {
A: 0.93,
operation: 'HTTP GET /dispatch',
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
},
},
{
data: {
A: 0.21,
operation: 'check_request limit',
resource_host_name: '',
service_name: 'demo-app',
},
},
{
data: {
A: 0.21,
operation: 'authenticate_check_cache',
resource_host_name: '',
service_name: 'demo-app',
},
},
{
data: {
A: 0.21,
operation: 'authenticate_check_db',
resource_host_name: '',
service_name: 'demo-app',
},
},
{
data: {
A: 0.21,
operation: 'authenticate',
resource_host_name: '',
service_name: 'demo-app',
},
},
{
data: {
A: 0.21,
operation: 'check cart in cache',
resource_host_name: '',
service_name: 'demo-app',
},
},
{
data: {
A: 0.2,
operation: 'get_cart',
resource_host_name: '',
service_name: 'demo-app',
},
},
{
data: {
A: 0.2,
operation: 'check cart in db',
resource_host_name: '',
service_name: 'demo-app',
},
},
{
data: {
A: 0.2,
operation: 'home',
resource_host_name: '',
service_name: 'demo-app',
},
},
],
},
},
],
},
},
params: {
start: 1726669030000,
end: 1726670830000,
step: 60,
variables: {},
formatForWeb: true,
compositeQuery: {
queryType: 'builder',
panelType: 'table',
fillGaps: false,
builderQueries: {
A: {
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_calls_total--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'signoz_calls_total',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: 'metrics',
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: 'string',
id: 'resource_host_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'resource_host_name',
type: 'tag',
},
{
dataType: 'string',
id: 'service_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
},
{
dataType: 'string',
id: 'operation--string--tag--false',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
},
],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
},
},
},
},
dataUpdatedAt: 1726670830710,
error: null,
errorUpdatedAt: 0,
failureCount: 0,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isRefetching: false,
isLoadingError: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isStale: true,
},
headerMenuList: ['view', 'clone', 'delete', 'edit'],
isWarning: false,
isFetchingResponse: false,
tableProcessedDataRef: {
current: [
{
resource_host_name: '4f6ec470feea',
service_name: 'redis',
operation: 'GetDriver',
A: 11.67,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
operation: 'HTTP GET',
A: 10.26,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
operation: 'HTTP GET: /route',
A: 9.33,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'route',
operation: 'HTTP GET /route',
A: 9.33,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'redis',
operation: 'FindDriverIDs',
A: 0.93,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
operation: 'HTTP GET: /customer',
A: 0.93,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'driver',
operation: '/driver.DriverService/FindNearest',
A: 0.93,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
operation: '/driver.DriverService/FindNearest',
A: 0.93,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'mysql',
operation: 'SQL SELECT',
A: 0.93,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'customer',
operation: 'HTTP GET /customer',
A: 0.93,
},
{
resource_host_name: '4f6ec470feea',
service_name: 'frontend',
operation: 'HTTP GET /dispatch',
A: 0.93,
},
{
resource_host_name: '',
service_name: 'demo-app',
operation: 'check_request limit',
A: 0.21,
},
{
resource_host_name: '',
service_name: 'demo-app',
operation: 'authenticate_check_cache',
A: 0.21,
},
{
resource_host_name: '',
service_name: 'demo-app',
operation: 'authenticate_check_db',
A: 0.21,
},
{
resource_host_name: '',
service_name: 'demo-app',
operation: 'authenticate',
A: 0.21,
},
{
resource_host_name: '',
service_name: 'demo-app',
operation: 'check cart in cache',
A: 0.21,
},
{
resource_host_name: '',
service_name: 'demo-app',
operation: 'get_cart',
A: 0.2,
},
{
resource_host_name: '',
service_name: 'demo-app',
operation: 'check cart in db',
A: 0.2,
},
{
resource_host_name: '',
service_name: 'demo-app',
operation: 'home',
A: 0.2,
},
],
},
};

View File

@@ -92,9 +92,10 @@ function ServiceMetricTable({
return (
<>
{RPS > MAX_RPS_LIMIT && (
<Flex justify="center">
<Flex justify="left">
<Typography.Title level={5} type="warning" style={{ marginTop: 0 }}>
<WarningFilled /> {getText('rps_over_100')}
<a href="mailto:cloud-support@signoz.io">email</a>
</Typography.Title>
</Flex>
)}

View File

@@ -49,10 +49,11 @@ function ServiceTraceTable({
return (
<>
{RPS > MAX_RPS_LIMIT && (
<Flex justify="flex-end">
<Typography.Text type="warning" style={{ marginTop: 0 }}>
<Flex justify="left">
<Typography.Title level={5} type="warning" style={{ marginTop: 0 }}>
<WarningFilled /> {getText('rps_over_100')}
</Typography.Text>
<a href="mailto:cloud-support@signoz.io">email</a>
</Typography.Title>
</Flex>
)}

View File

@@ -3,10 +3,6 @@
height: 100%;
position: relative;
z-index: 1;
&.docked {
width: 240px;
}
}
.sideNav {
@@ -229,39 +225,6 @@
display: block;
}
}
&.docked {
flex: 0 0 240px;
max-width: 240px;
min-width: 240px;
width: 240px;
.secondary-nav-items {
width: 240px;
}
.brand {
justify-content: space-between;
}
.get-started-nav-items {
.get-started-btn {
justify-content: flex-start;
}
}
.collapse-expand-handlers {
display: block;
}
.nav-item-label {
display: block;
}
.nav-item-beta {
display: block;
}
}
}
.lightMode {

View File

@@ -3,7 +3,7 @@
import './SideNav.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Tooltip } from 'antd';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
@@ -16,9 +16,6 @@ import history from 'lib/history';
import {
AlertTriangle,
CheckSquare,
ChevronLeftCircle,
ChevronRightCircle,
PanelRight,
RocketIcon,
UserCircle,
} from 'lucide-react';
@@ -55,13 +52,9 @@ interface UserManagementMenuItems {
function SideNav({
licenseData,
isFetching,
onCollapse,
collapsed,
}: {
licenseData: any;
isFetching: boolean;
onCollapse: () => void;
collapsed: boolean;
}): JSX.Element {
const [menuItems, setMenuItems] = useState(defaultMenuItems);
@@ -330,8 +323,6 @@ function SideNav({
};
useEffect(() => {
registerShortcut(GlobalShortcuts.SidebarCollapse, onCollapse);
registerShortcut(GlobalShortcuts.NavigateToServices, () =>
onClickHandler(ROUTES.APPLICATION, null),
);
@@ -359,7 +350,6 @@ function SideNav({
);
return (): void => {
deregisterShortcut(GlobalShortcuts.SidebarCollapse);
deregisterShortcut(GlobalShortcuts.NavigateToServices);
deregisterShortcut(GlobalShortcuts.NavigateToTraces);
deregisterShortcut(GlobalShortcuts.NavigateToLogs);
@@ -368,11 +358,11 @@ function SideNav({
deregisterShortcut(GlobalShortcuts.NavigateToExceptions);
deregisterShortcut(GlobalShortcuts.NavigateToMessagingQueues);
};
}, [deregisterShortcut, onClickHandler, onCollapse, registerShortcut]);
}, [deregisterShortcut, onClickHandler, registerShortcut]);
return (
<div className={cx('sidenav-container', !collapsed ? 'docked' : '')}>
<div className={cx('sideNav', !collapsed ? 'docked' : '')}>
<div className={cx('sidenav-container')}>
<div className={cx('sideNav')}>
<div className="brand">
<div className="brand-company-meta">
<div
@@ -392,17 +382,6 @@ function SideNav({
<div className="license tag nav-item-label">{licenseTag}</div>
)}
</div>
<Tooltip
title={collapsed ? 'Dock Sidebar' : 'Undock Sidebar'}
placement="right"
>
<Button
className="periscope-btn nav-item-label dockBtn"
icon={<PanelRight size={16} />}
onClick={onCollapse}
/>
</Tooltip>
</div>
{isCloudUserVal && (
@@ -504,14 +483,6 @@ function SideNav({
}}
/>
)}
<div className="collapse-expand-handlers" onClick={onCollapse}>
{collapsed ? (
<ChevronRightCircle size={18} />
) : (
<ChevronLeftCircle size={18} />
)}
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,6 @@
import { RocketOutlined } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import {
AreaChart,
BarChart2,
BellDot,
BugIcon,
@@ -114,11 +113,6 @@ const menuItems: SidebarItem[] = [
icon: <Route size={16} />,
isBeta: true,
},
{
key: ROUTES.USAGE_EXPLORER,
label: 'Usage Explorer',
icon: <AreaChart size={16} />,
},
{
key: ROUTES.BILLING,
label: 'Billing',

View File

@@ -14,9 +14,8 @@ import { Pagination } from 'hooks/queryPagination';
import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import useUrlQueryData from 'hooks/useUrlQueryData';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { HTMLAttributes, memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
@@ -25,7 +24,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { TracesLoading } from '../TraceLoading/TraceLoading';
import { defaultSelectedColumns, PER_PAGE_OPTIONS } from './configs';
import { Container, ErrorText, tableStyles } from './styles';
import { getListColumns, getTraceLink, transformDataWithDate } from './utils';
import { getListColumns, transformDataWithDate } from './utils';
interface ListViewProps {
isFilterApplied: boolean;
@@ -108,21 +107,6 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
[queryTableData],
);
const handleRow = useCallback(
(record: RowData): HTMLAttributes<RowData> => ({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getTraceLink(record), '_blank');
} else {
history.push(getTraceLink(record));
}
},
}),
[],
);
const handleDragColumn = useCallback(
(fromIndex: number, toIndex: number) =>
onDragColumns(columns, fromIndex, toIndex),
@@ -169,7 +153,6 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
style={tableStyles}
dataSource={transformedQueryTableData}
columns={columns}
onRow={handleRow}
onDragColumn={handleDragColumn}
/>
)}

View File

@@ -47,11 +47,11 @@ export const getListColumns = (
key: 'date',
title: 'Timestamp',
width: 145,
render: (item): JSX.Element => {
render: (value, item): JSX.Element => {
const date =
typeof item === 'string'
? dayjs(item).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(item / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
typeof value === 'string'
? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return (
<BlockLink to={getTraceLink(item)}>
<Typography.Text>{date}</Typography.Text>
@@ -67,10 +67,10 @@ export const getListColumns = (
dataIndex: key,
key: `${key}-${dataType}-${type}`,
width: 145,
render: (value): JSX.Element => {
render: (value, item): JSX.Element => {
if (value === '') {
return (
<BlockLink to={getTraceLink(value)}>
<BlockLink to={getTraceLink(item)}>
<Typography data-testid={key}>N/A</Typography>
</BlockLink>
);
@@ -78,7 +78,7 @@ export const getListColumns = (
if (key === 'httpMethod' || key === 'responseStatusCode') {
return (
<BlockLink to={getTraceLink(value)}>
<BlockLink to={getTraceLink(item)}>
<Tag data-testid={key} color="magenta">
{value}
</Tag>
@@ -88,14 +88,14 @@ export const getListColumns = (
if (key === 'durationNano') {
return (
<BlockLink to={getTraceLink(value)}>
<BlockLink to={getTraceLink(item)}>
<Typography data-testid={key}>{getMs(value)}ms</Typography>
</BlockLink>
);
}
return (
<BlockLink to={getTraceLink(value)}>
<BlockLink to={getTraceLink(item)}>
<Typography data-testid={key}>{value}</Typography>
</BlockLink>
);

View File

@@ -52,6 +52,7 @@
height: 40px;
justify-content: end;
padding: 0 8px;
margin: 12px 0 2px;
}
}

View File

@@ -70,7 +70,7 @@ export const useFetchKeysAndValues = (
const queryFiltersWithoutId = useMemo(
() => ({
...query.filters,
items: query.filters.items.map((item) => {
items: query.filters?.items?.map((item) => {
const filterWithoutId = cloneDeep(item);
unset(filterWithoutId, 'id');
return filterWithoutId;

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