Compare commits

...

96 Commits

Author SHA1 Message Date
Shivanshu Raj Shrivastava
e51f4d986d feat: kafka Scenario 1, 3, 4 all squashed (#6144)
* feat: kafka partition level observerability features
2024-10-17 19:44:42 +05:30
Vishal Sharma
337a941d0d feat: onboarding API via proxy (#6058)
* feat: onboarding API via proxy

* chore: update profiles route

* chore: update profiles url
2024-10-17 19:09:10 +05:30
Vishal Sharma
fc4b55cb34 feat: bulk invite user api (#6057) 2024-10-17 18:41:31 +05:30
Shaheer Kochai
96cb8053df chore: add test id to additional filters button (#6183) 2024-10-17 10:13:35 +04:30
Shaheer Kochai
5651d69485 fix: add v4 to the new alert payload (#6090) 2024-10-17 10:12:26 +04:30
Prashant Shahi
a6e492880d fix(docker): use env prefix for boolean in collector config (#6199)
### Summary

Fixes for `expected type 'bool', got unconvertible type 'string'` errors.

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-10-16 15:07:02 +05:30
SagarRajput-7
80b3c3e256 fix: fixed all not deselecting and empty array setting to _ALL_ (#6086) 2024-10-16 11:22:30 +05:30
Srikanth Chekuri
0806420dd7 chore: add process list (#6125) 2024-10-15 23:02:52 +05:30
Yunus M
18e240e3d1 feat: search series in anomaly response data (#6185) 2024-10-15 19:16:43 +05:30
Prashant Shahi
d0965a24c5 Merge pull request #6181 from SigNoz/sync/signoz-0.56.0
Sync/post release v0.56
2024-10-15 00:11:37 +05:30
Robi
7ed689693f fix: remove trailing slash from http payload (#6176) 2024-10-14 19:46:45 +05:30
rahulkeswani101
90ae55264a fix: updated row key in triggered alert list table (#6154) 2024-10-14 17:16:44 +05:30
Srikanth Chekuri
bf4c792cdb chore: update default feature flag and error response for formula (#6184) 2024-10-14 14:04:49 +05:30
SagarRajput-7
dd097821d1 fix: fixed incorrect label for orderBy clause when selected (#6177) 2024-10-14 14:04:27 +05:30
Yunus M
701b8803ac feat: move anomaly detection behind ff and show beta (#6180) 2024-10-14 12:18:55 +05:30
Raj Kamal Singh
2728ddd255 Fix: log pipelines generates bad config if first op is disabled (#6174)
* chore: add test reproducing bad config generation when first pipeline op is disabled

* fix: logs pipelines: set router output to first enabled operator
2024-10-14 11:24:42 +05:30
Raj Kamal Singh
5187ed58a0 Revert "Revert "Feat: use new logspipelineprocessor for generating logs pipel…" (#6179)
This reverts commit 1411ae41c3.
2024-10-14 11:11:51 +05:30
Yunus M
2180118094 feat: anomaly detection UI (#5916)
* feat: anamoly detection - initial ui

* feat: anamoly detection - oct 10

* feat: use antd checkbox

* feat: handle multiple series

* feat: handle chart height on switch btwn threshold / anomaly

* feat: do not update url on detection type change

* chore: pr clean up

* feat: remove chart container background
2024-10-14 10:31:02 +05:30
Ankit Anand
ecae842fa1 chore: Update README.md (#6172)
* Update README.md

* Update README.md

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prateek <pranay@signoz.io>
2024-10-13 11:02:04 +05:30
hulk
291b3ba357 perf(cache): should delete multiple keys at once to reduce operations in Redis cache (#6170) 2024-10-12 19:55:18 +05:30
Prashant Shahi
78d1e19e60 Merge pull request #6156 from SigNoz/release/v0.56.x
Release/v0.56.x
2024-10-11 21:41:34 +05:30
Yunus M
fa9e89bfe7 chore: update body parser (#6165) 2024-10-11 10:32:10 +05:30
Yunus M
16f49a1d25 Revert "chore(deps): bump uplot from 1.6.26 to 1.6.31 in /frontend (#6104)" (#6163)
This reverts commit c95c0f9a15.
2024-10-11 09:42:59 +05:30
dependabot[bot]
c95c0f9a15 chore(deps): bump uplot from 1.6.26 to 1.6.31 in /frontend (#6104)
Bumps [uplot](https://github.com/leeoniya/uPlot) from 1.6.26 to 1.6.31.
- [Release notes](https://github.com/leeoniya/uPlot/releases)
- [Commits](https://github.com/leeoniya/uPlot/compare/1.6.26...1.6.31)

---
updated-dependencies:
- dependency-name: uplot
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-10 20:39:48 +05:30
Prashant Shahi
5588c7dd3f revert(signoz): pin versions: Schema Migrator 0.102.10
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-10-10 19:41:47 +05:30
Ekansh Gupta
679b5db5a2 #5861 Changes query which improves performance (#6081)
* fix(query): #5861 Changes query which improves performance

* fix(test): fixed all the build test

* fix(test): fixed all the build test

* fix(test): fixed all the build test

* fix(test): fixed all the build test

* fix(test): fixed all the build test

* fix(test): fixed all the build test
2024-10-10 18:16:11 +05:30
dependabot[bot]
64feff3539 chore(deps): bump dompurify from 2.4.7 to 3.1.3 in /frontend (#6157)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.4.7 to 3.1.3.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.4.7...3.1.3)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-10 17:23:47 +05:30
Srikanth Chekuri
1720d616f6 chore: add hosts list support (#6123) 2024-10-10 17:02:46 +05:30
Srikanth Chekuri
155a2ea557 chore: skip showing metrics with dot in name (#6096) 2024-10-10 17:01:44 +05:30
dependabot[bot]
d5c38ed0a4 chore(deps): bump dompurify from 3.0.0 to 3.1.3 in /frontend (#5985)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.0.0 to 3.1.3.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.0.0...3.1.3)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-10 16:59:28 +05:30
Prashant Shahi
b70d50f2b3 chore: pin versions: SigNoz 0.56.0, OtelCollector 0.102.12, Alertmanager 0.23.7
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-10-10 15:39:49 +05:30
Prashant Shahi
728f699051 Merge branch 'main' into release/v0.56.x 2024-10-10 15:17:47 +05:30
Srikanth Chekuri
3bbbc759d3 chore: bump SigNoz/prometheus (#6095) 2024-10-10 14:10:28 +05:30
Nityananda Gohain
2230ca1740 fix: enrich attributes regardless if it is materialized (#6000)
* fix: enrich attributes regardless if it is materialized

* feat: take care of same key name with different type and datatype

* fix: makeLinks updated with new logic for logs

* fix: clean up PrepareFilters and make it generic
2024-10-09 20:03:26 +05:30
SagarRajput-7
440fd4e02b feat: added info funct for panels in dashaboard layout for showing description (#6133)
* feat: added info funct for panels in dashaboard layout for showing description

* feat: position description info next to title

* feat: replaced error info icon
2024-10-09 13:33:14 +05:30
Abhishek Mehandiratta
78a924d378 fix(dashboard): initial title value in rename modal set to currently selected dashboard (#5821)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
2024-10-09 13:32:17 +05:30
SagarRajput-7
b03fadc2ec chore: hide promql from panel type - pie (#6140) 2024-10-09 13:31:59 +05:30
Vikrant Gupta
4b79d3b785 fix: incorrect query being generated from traces page (#6130) 2024-10-09 11:41:38 +05:30
SagarRajput-7
a24fb5d84f fix: hide PromQL from table panel type (#6117)
* fix: hide PromQL from table panel type

* fix: handled switch back to query tab if promql was selected earlier

* fix: made a constant for panel-type to query-type
2024-10-09 11:27:47 +05:30
Vikrant Gupta
137059ded6 chore: added some easter eggs (#6136)
* chore: added easter egg

* chore: show all
2024-10-09 11:26:55 +05:30
Shaheer Kochai
f1ce82ac25 feat: client side query builder search (#5891)
* feat: build client side QB search

* feat: query builder light mode support + overall UI improvements

* fix: preserve the alert rule labels in context

* feat: get labels and all possible values from /timeline API

* chore: remove unnecessary dropdownRender and optional fields from AttributeKey

* chore: merge the styles of .tag

* chore: use the correct type for attributeKeys

* chore: use the correct values for alert rule state in the context
2024-10-09 09:29:44 +04:30
Srikanth Chekuri
4aeed392d7 chore: do not materialize ttl after modify (#6106) 2024-10-08 19:35:38 +05:30
Srikanth Chekuri
4356ddae8c chore: keep anomaly response sync with v4 query range (#6113) 2024-10-08 13:33:33 +05:30
Sergei Zobov
76e7de3aed docker setup: docker compose without testing app (#5839)
I preserved the existing interfaces, so:
`docker compose -f docker/clickhouse-setup/docker-compose.yaml up -d`,  would run the same set of services (with the testing app).

The interface that was added is:
`docker compose -f docker/clickhouse-setup/docker-compose-minimal.yaml up -d`, which won't run testing app.
2024-10-07 18:24:50 +05:30
rahulkeswani101
ae5e63cc64 fix: updated the triggered alert list code (#6127) 2024-10-07 12:14:09 +05:30
Shaheer Kochai
5ef05891ce fix: fix incorrect alert history state (#5898)
* fix: on unmount remove the  alert disabled state

* fix: get updated alert state from response and fix the alert state mismatch issue
2024-10-07 09:57:46 +04:30
Shaheer Kochai
c452e23b18 chore: changes for new alert e2e tests (#6089)
* chore: changes for new alert version related tests

* chore: add test ids
2024-10-07 09:56:08 +04:30
rahulkeswani101
69aab87d72 Merge pull request #5826 from SigNoz/SIG-5729
feat: added view logs button for error and latency chart
2024-10-04 20:04:50 +05:30
rahulkeswani101
a60674cf1b Merge branch 'develop' into SIG-5729 2024-10-04 19:54:39 +05:30
Shivanshu Raj Shrivastava
022b9226a7 Merge pull request #6097 from shivanshuraj1333/feat/issues/1834
Add onboarding APIs to check the attributes for Messaging Queues feature
2024-10-04 18:23:49 +05:30
rahulkeswani101
36e2404814 Merge branch 'develop' into SIG-5729 2024-10-04 16:10:54 +05:30
Shaheer Kochai
2eb3f6cb06 feat: store columns while saving view and restore columns on selecting view without select columns (#5647)
* feat: store columns while saving view and restore columns on selecting view without select columns

* fix: add null check to prevent storing empty selectItems

* fix: restore the default select columns and remove OLD_SELECT_COLUMNS

* chore: pr review changes
2024-10-04 11:37:19 +05:30
shivanshu
98cbdf570f feat: api documentation and nits 2024-10-04 01:44:52 +05:30
shivanshu
d380894c35 feat: a bunch of advancements, query optimisation, new response format 2024-10-04 01:11:05 +05:30
shivanshu
ea0263cc73 feat: onboarding APIs 2024-10-04 01:11:05 +05:30
shivanshu
f38a1d9f1c feat: add the queries 2024-10-04 01:11:05 +05:30
Srikanth Chekuri
9390a815a8 feat: add dot support for alerts (#6062) 2024-10-03 16:56:58 +05:30
Srikanth Chekuri
4f76e13dbe feat: add ability to configure number of required points (#5242) 2024-10-03 16:48:32 +05:30
rahulkeswani101
6a4643558c feat: added step interval instead of tplusone 2024-10-03 15:36:17 +05:30
Shaheer Kochai
a98c8db949 feat: add drag support to alert history horizontal graph (#5928)
* feat: add drag support to alert history horizontal graph

* chore: use startTimestamp and endTimestamp

* fix: fix the issue of alert history breaking on navigating back from selected time range
2024-10-03 11:09:18 +04:30
rahulkeswani101
5ba9c9d48c fix: added custom breakdown of one day to handle billing graph issue. (#5994)
* fix: added custom breakdown to handle billing graph issue

* chore: remove console statement

* chore: added comment for current implementation of adding next day details in breakdown
2024-10-01 19:17:43 +05:30
Yunus M
e1ca71dcea fix: get started on available for cloud users (#6103)
Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-10-01 13:32:11 +05:30
Shaheer Kochai
266ed58908 feat: add expand/collapse button to the top in trace details (#5980)
* feat: add expand/collapse button to the top in trace details

* fix: make the trace details collapsed sidebar match the design

* fix: failing test by modifying the expand button class name
2024-10-01 12:17:15 +04:30
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
rahulkeswani101
c7bd7566c5 Merge branch 'develop' into SIG-5729 2024-09-26 17:28:49 +05:30
rahulkeswani101
f4fbe62169 feat: added new function to decide new path based on button clicked 2024-09-26 17:26:14 +05:30
Prashant Shahi
87499d1ead feat: enable new logs schema by default (#6077)
#### Summary

- update all docker compose YAMLs to use new logs schema
- enable `use_new_schema ` flag for clickhouselogsexporter in otel-collector config YAMLs
- remove prefer delta from docker-compose YAMLs

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-09-26 00:31:06 +05:30
Prashant Shahi
5fa8686fcf Merge pull request #6075 from SigNoz/release/v0.55.x
Release/v0.55.x
2024-09-25 22:25:37 +05:30
Prashant Shahi
dc2db524c7 chore(signoz): 📌 pin versions: SigNoz 0.55.0, SigNoz OtelCollector 0.102.10
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-09-25 21:46:12 +05:30
Prashant Shahi
b3545b767a Merge branch 'main' into release/v0.55.x 2024-09-25 21:43:48 +05:30
rahulkeswani101
6685482ea6 Merge branch 'develop' into SIG-5729 2024-09-25 11:04:10 +05:30
rahulkeswani101
540a2c6712 feat: added all values for severity text when we are navigating from error panel to logs 2024-09-16 15:49:55 +05:30
Prashant Shahi
08f3b089f4 Merge pull request #5922 from SigNoz/release/v0.54.x
Release/v0.54.x
2024-09-11 15:33:09 +05:30
Prashant Shahi
1d8e5b6c0f chore(signoz): 📌 pin versions: SigNoz 0.54.0, SigNoz OtelCollector 0.102.8
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-09-11 13:47:02 +05:30
Prashant Shahi
0dcded59e5 Merge branch 'main' into release/v0.54.x 2024-09-11 13:45:00 +05:30
rahulkeswani101
bfb63ca8c4 fix: custom end time issue while navigating to a different page 2024-09-08 22:22:39 +05:30
rahulkeswani101
71e24483dd Merge branch 'develop' into SIG-5729 2024-09-08 22:01:48 +05:30
rahulkeswani101
317c41a166 Merge branch 'develop' into SIG-5729 2024-09-06 12:22:41 +05:30
rahulkeswani101
ed4613cb1b feat: added severity text as a filter and removed relative time param from url 2024-09-04 15:35:01 +05:30
rahulkeswani101
6c06fea1aa style: remove unused CSS 2024-09-02 13:43:54 +05:30
rahulkeswani101
6bc2f9125c feat: added view logs button for error and latency chart 2024-09-02 09:54:49 +05:30
Prashant Shahi
262beef8f9 Merge pull request #5800 from SigNoz/release/v0.53.x
Release/v0.53.x
2024-08-30 15:20:27 +05:30
Prashant Shahi
43cc6dea92 chore(signoz): 📌 pin versions: SigNoz OtelCollector 0.102.7
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-08-30 15:06:52 +05:30
Prashant Shahi
6684640abe Merge branch 'develop' into release/v0.53.x 2024-08-30 12:50:35 +05:30
Prashant Shahi
0a146910d6 chore(signoz): 📌 pin versions: SigNoz 0.53.0, SigNoz OtelCollector 0.102.6
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-08-29 19:25:34 +05:30
Prashant Shahi
690ed0f7f1 Merge branch 'main' into release/v0.53.x 2024-08-29 19:23:57 +05:30
Prashant Shahi
5bcf7de440 Merge pull request #5704 from SigNoz/release/v0.52.x
Release/v0.52.x
2024-08-16 21:00:32 +05:30
Prashant Shahi
703983a5f9 chore(signoz): 📌 pin versions: SigNoz 0.52.0, SigNoz OtelCollector 0.102.3
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-08-15 23:12:36 +05:30
Prashant Shahi
766a2123c5 Merge branch 'main' into release/v0.52.x 2024-08-15 13:42:02 +05:30
Prashant Shahi
a476c68f7e Merge pull request #5618 from SigNoz/release/v0.51.x
Release/v0.51.x
2024-07-31 22:30:09 +05:30
Prashant Shahi
fc15aa6f1c Merge branch 'develop' into release/v0.51.x 2024-07-31 21:29:07 +05:30
Prashant Shahi
4192fd573d chore(signoz): 📌 pin versions: SigNoz 0.51.0, SigNoz OtelCollector 0.102.3
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-07-31 20:29:09 +05:30
Prashant Shahi
ca13d80205 Merge branch 'main' into release/v0.51.x 2024-07-31 20:27:51 +05:30
Prashant Shahi
8d84ce8f06 Merge pull request #5509 from SigNoz/release/v0.50.x
Release/v0.50.x
2024-07-17 20:16:19 +05:30
Prashant Shahi
09ea7b9eb5 chore(signoz): 📌 pin versions: SigNoz 0.50.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-07-17 19:01:03 +05:30
159 changed files with 8120 additions and 1328 deletions

200
README.md
View File

@@ -1,8 +1,11 @@
<p align="center">
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
<h1 align="center" style="border-bottom: none">
<a href="https://signoz.io" target="_blank">
<img alt="SigNoz" src="https://github.com/user-attachments/assets/ef9a33f7-12d7-4c94-8908-0a02b22f0c18" width="100" height="100">
</a>
<br>SigNoz
</h1>
<p align="center">Monitor your applications and troubleshoot problems in your deployed applications, an open-source alternative to DataDog, New Relic, etc.</p>
</p>
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
<p align="center">
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Docker Downloads"> </a>
@@ -21,55 +24,115 @@
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
</h3>
##
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. With SigNoz, you can:
👉 Visualise Metrics, Traces and Logs in a single pane of glass
👉 You can see metrics like p99 latency, error rates for your services, external API calls and individual end points.
👉 You can find the root cause of the problem by going to the exact traces which are causing the problem and see detailed flamegraphs of individual request traces.
👉 Run aggregates on trace data to get business relevant metrics
👉 Filter and query logs, build dashboards and alerts based on attributes in logs
👉 Record exceptions automatically in Python, Java, Ruby, and Javascript
👉 Easy to set alerts with DIY query builder
## Features
### Application Metrics
### Application Performance Monitoring
![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png)
Use SigNoz APM to monitor your applications and services. It comes with out-of-box charts for key application metrics like p99 latency, error rate, Apdex and operations per second. You can also monitor the database and external calls made from your application. Read [more](https://signoz.io/application-performance-monitoring/).
You can [instrument](https://signoz.io/docs/instrumentation/) your application with OpenTelemetry to get started.
### Distributed Tracing
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
![apm-cover](https://github.com/user-attachments/assets/fa5c0396-0854-4c8b-b972-9b62fd2a70d2)
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
### Logs Management
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
SigNoz can be used as a centralized log management solution. We use ClickHouse (used by likes of Uber & Cloudflare) as a datastore, ⎯ an extremely fast and highly optimized storage for logs data. Instantly search through all your logs using quick filters and a powerful query builder.
### Infrastructure Monitoring
You can also create charts on your logs and monitor them with customized dashboards. Read [more](https://signoz.io/log-management/).
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
![logs-management-cover](https://github.com/user-attachments/assets/343588ee-98fb-4310-b3d2-c5bacf9c7384)
### Exceptions Monitoring
![exceptions_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.png)
### Distributed Tracing
Distributed Tracing is essential to troubleshoot issues in microservices applications. Powered by OpenTelemetry, distributed tracing in SigNoz can help you track user requests across services to help you identify performance bottlenecks.
See user requests in a detailed breakdown with the help of Flamegraphs and Gantt Charts. Click on any span to see the entire trace represented beautifully, which will help you make sense of where issues actually occurred in the flow of requests.
Read [more](https://signoz.io/distributed-tracing/).
![distributed-tracing-cover](https://github.com/user-attachments/assets/9bfe060a-0c40-4922-9b55-8a97e1a4076c)
### Metrics and Dashboards
Ingest metrics from your infrastructure or applications and create customized dashboards to monitor them. Create visualization that suits your needs with a variety of panel types like pie chart, time-series, bar chart, etc.
Create queries on your metrics data quickly with an easy-to-use metrics query builder. Add multiple queries and combine those queries with formulae to create really complex queries quickly.
Read [more](https://signoz.io/metrics-and-dashboards/).
![metrics-n-dashboards-cover](https://github.com/user-attachments/assets/a536fd71-1d2c-4681-aa7e-516d754c47a5)
### Alerts
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
Use alerts in SigNoz to get notified when anything unusual happens in your application. You can set alerts on any type of telemetry signal (logs, metrics, traces), create thresholds and set up a notification channel to get notified. Advanced features like alert history and anomaly detection can help you create smarter alerts.
Alerts in SigNoz help you identify issues proactively so that you can address them before they reach your customers.
Read [more](https://signoz.io/alerts-management/).
![alerts-cover](https://github.com/user-attachments/assets/03873bb8-1b62-4adf-8f56-28bb7b1750ea)
### Exceptions Monitoring
Monitor exceptions automatically in Python, Java, Ruby, and Javascript. For other languages, just drop in a few lines of code and start monitoring exceptions.
See the detailed stack trace for all exceptions caught in your application. You can also log in custom attributes to add more context to your exceptions. For example, you can add attributes to identify users for which exceptions occurred.
Read [more](https://signoz.io/exceptions-monitoring/).
![exceptions-cover](https://github.com/user-attachments/assets/4be37864-59f2-4e8a-8d6e-e29ad04298c5)
<br /><br />
## Why SigNoz?
SigNoz is a single tool for all your monitoring and observability needs. Here are a few reasons why you should choose SigNoz:
- Single tool for observability(logs, metrics, and traces)
- Built on top of [OpenTelemetry](https://opentelemetry.io/), the open-source standard which frees you from any type of vendor lock-in
- Correlated logs, metrics and traces for much richer context while debugging
- Uses ClickHouse (used by likes of Uber & Cloudflare) as datastore - an extremely fast and highly optimized storage for observability data
- DIY Query builder, PromQL, and ClickHouse queries to fulfill all your use-cases around querying observability data
- Open-Source - you can use open-source, our [cloud service](https://signoz.io/teams/) or a mix of both based on your use case
## Getting Started
### Create a SigNoz Cloud Account
SigNoz cloud is the easiest way to get started with SigNoz. Our cloud service is for those users who want to spend more time in getting insights for their application performance without worrying about maintenance.
[Get started for free](https://signoz.io/teams/)
### Deploy using Docker(self-hosted)
Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
<p>&nbsp </p>
### Deploy in Kubernetes using Helm(self-hosted)
Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_chart) to install using helm charts
<br /><br />
We also offer managed services in your infra. Check our [pricing plans](https://signoz.io/pricing/) for all details.
## Join our Slack community
@@ -78,64 +141,22 @@ Come say Hi to us on [Slack](https://signoz.io/slack) 👋
<br /><br />
## Features:
- Unified UI for metrics, traces and logs. No need to switch from Prometheus to Jaeger to debug issues, or use a logs tool like Elastic separate from your metrics and traces stack.
- Application overview metrics like RPS, 50th/90th/99th Percentile latencies, and Error Rate
- Slowest endpoints in your application
- See exact request trace to figure out issues in downstream services, slow DB queries, call to 3rd party services like payment gateways, etc
- Filter traces by service name, operation, latency, error, tags/annotations.
- Run aggregates on trace data (events/spans) to get business relevant metrics. e.g. You can get error rate and 99th percentile latency of `customer_type: gold` or `deployment_version: v2` or `external_call: paypal`
- Native support for OpenTelemetry Logs, advanced log query builder, and automatic log collection from k8s cluster
- Lightning quick log analytics ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/))
- End-to-End visibility into infrastructure performance, ingest metrics from all kinds of host environments
- Easy to set alerts with DIY query builder
<br /><br />
## Why SigNoz?
Being developers, we found it annoying to rely on closed source SaaS vendors for every small feature we wanted. Closed source vendors often surprise you with huge month end bills without any transparency.
We wanted to make a self-hosted & open source version of tools like DataDog, NewRelic for companies that have privacy and security concerns about having customer data going to third party services.
Being open source also gives you complete control of your configuration, sampling, uptimes. You can also build modules over SigNoz to extend business specific capabilities
### Languages supported:
We support [OpenTelemetry](https://opentelemetry.io) as the library which you can use to instrument your applications. So any framework and language supported by OpenTelemetry is also supported by SigNoz. Some of the main supported languages are:
SigNoz supports all major programming languages for monitoring. Any framework and language supported by OpenTelemetry is supported by SigNoz. Find instructions for instrumenting different languages below:
- Java
- Python
- Node.js
- Go
- PHP
- .NET
- Ruby
- Elixir
- Rust
- [Java](https://signoz.io/docs/instrumentation/java/)
- [Python](https://signoz.io/docs/instrumentation/python/)
- [Node.js or Javascript](https://signoz.io/docs/instrumentation/javascript/)
- [Go](https://signoz.io/docs/instrumentation/golang/)
- [PHP](https://signoz.io/docs/instrumentation/php/)
- [.NET](https://signoz.io/docs/instrumentation/dotnet/)
- [Ruby](https://signoz.io/docs/instrumentation/ruby-on-rails/)
- [Elixir](https://signoz.io/docs/instrumentation/elixir/)
- [Rust](https://signoz.io/docs/instrumentation/rust/)
- [Swift](https://signoz.io/docs/instrumentation/swift/)
You can find the complete list of languages here - https://opentelemetry.io/docs/
<br /><br />
## Getting Started
### Deploy using Docker
Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
<p>&nbsp </p>
### Deploy in Kubernetes using Helm
Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_chart) to install using helm charts
You can find our entire documentation [here](https://signoz.io/docs/introduction/).
<br /><br />
@@ -144,9 +165,11 @@ Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_cha
### SigNoz vs Prometheus
Prometheus is good if you want to do just metrics. But if you want to have a seamless experience between metrics and traces, then current experience of stitching together Prometheus & Jaeger is not great.
Prometheus is good if you want to do just metrics. But if you want to have a seamless experience between metrics, logs and traces, then current experience of stitching together Prometheus & other tools is not great.
Our goal is to provide an integrated UI between metrics & traces - similar to what SaaS vendors like Datadog provides - and give advanced filtering and aggregation over traces, something which Jaeger currently lack.
SigNoz is a one-stop solution for metrics and other telemetry signals. And because you will use the same standard(OpenTelemetry) to collect all telemetry signals, you can also correlate these signals to troubleshoot quickly.
For example, if you see that there are issues with infrastructure metrics of your k8s cluster at a timestamp, you can jump to other signals like logs and traces to understand the issue quickly.
<p>&nbsp </p>
@@ -158,6 +181,7 @@ Moreover, SigNoz has few more advanced features wrt Jaeger:
- Jaegar UI doesnt show any metrics on traces or on filtered traces
- Jaeger cant get aggregates on filtered traces. For example, p99 latency of requests which have tag - customer_type='premium'. This can be done easily on SigNoz
- You can also go from traces to logs easily in SigNoz
<p>&nbsp </p>

View File

@@ -133,7 +133,7 @@ services:
# - ./data/clickhouse-3/:/var/lib/clickhouse/
alertmanager:
image: signoz/alertmanager:0.23.5
image: signoz/alertmanager:0.23.7
volumes:
- ./data/alertmanager:/data
command:
@@ -146,11 +146,11 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.49.1
image: signoz/query-service:0.56.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.56.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.12
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

@@ -131,8 +131,8 @@ processors:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
docker_multi_node_cluster: ${env:DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
@@ -142,8 +142,9 @@ exporters:
# logging: {}
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
docker_multi_node_cluster: ${env:DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
use_new_schema: true
extensions:
health_check:
endpoint: 0.0.0.0:13133

View File

@@ -1,5 +1,8 @@
version: "2.4"
include:
- test-app-docker-compose.yaml
services:
zookeeper-1:
image: bitnami/zookeeper:3.7.1
@@ -54,7 +57,7 @@ services:
alertmanager:
container_name: signoz-alertmanager
image: signoz/alertmanager:0.23.5
image: signoz/alertmanager:0.23.7
volumes:
- ./data/alertmanager:/data
depends_on:
@@ -66,7 +69,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 +84,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.12
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -128,29 +131,3 @@ services:
depends_on:
- otel-collector
restart: on-failure
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: [ "all" ]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "signoz/locust:1.2.3"
container_name: load-hotrod
hostname: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../common/locust-scripts:/locust

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

@@ -0,0 +1,279 @@
x-clickhouse-defaults: &clickhouse-defaults
restart: on-failure
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:24.1.2-alpine
tty: true
depends_on:
- zookeeper-1
# - zookeeper-2
# - zookeeper-3
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test:
[
"CMD",
"wget",
"--spider",
"-q",
"0.0.0.0:8123/ping"
]
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
x-db-depend: &db-depend
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator:
condition: service_completed_successfully
# clickhouse-2:
# condition: service_healthy
# clickhouse-3:
# condition: service_healthy
services:
zookeeper-1:
image: bitnami/zookeeper:3.7.1
container_name: signoz-zookeeper-1
hostname: zookeeper-1
user: root
ports:
- "2181:2181"
- "2888:2888"
- "3888:3888"
volumes:
- ./data/zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
# - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
# zookeeper-2:
# image: bitnami/zookeeper:3.7.0
# container_name: signoz-zookeeper-2
# hostname: zookeeper-2
# user: root
# ports:
# - "2182:2181"
# - "2889:2888"
# - "3889:3888"
# volumes:
# - ./data/zookeeper-2:/bitnami/zookeeper
# environment:
# - ZOO_SERVER_ID=2
# - ZOO_SERVERS=zookeeper-1:2888:3888,0.0.0.0:2888:3888,zookeeper-3:2888:3888
# - ALLOW_ANONYMOUS_LOGIN=yes
# - ZOO_AUTOPURGE_INTERVAL=1
# zookeeper-3:
# image: bitnami/zookeeper:3.7.0
# container_name: signoz-zookeeper-3
# hostname: zookeeper-3
# user: root
# ports:
# - "2183:2181"
# - "2890:2888"
# - "3890:3888"
# volumes:
# - ./data/zookeeper-3:/bitnami/zookeeper
# environment:
# - ZOO_SERVER_ID=3
# - ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
# - ALLOW_ANONYMOUS_LOGIN=yes
# - ZOO_AUTOPURGE_INTERVAL=1
clickhouse:
<<: *clickhouse-defaults
container_name: signoz-clickhouse
hostname: clickhouse
ports:
- "9000:9000"
- "8123:8123"
- "9181:9181"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
- ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/
- ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-2:
# <<: *clickhouse-defaults
# container_name: signoz-clickhouse-2
# hostname: clickhouse-2
# ports:
# - "9001:9000"
# - "8124:8123"
# - "9182:9181"
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-2/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-3:
# <<: *clickhouse-defaults
# container_name: signoz-clickhouse-3
# hostname: clickhouse-3
# ports:
# - "9002:9000"
# - "8125:8123"
# - "9183:9181"
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-3/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
container_name: signoz-alertmanager
volumes:
- ./data/alertmanager:/data
depends_on:
query-service:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8085
- --storage.path=/data
# 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.56.0}
container_name: signoz-query-service
command:
[
"-config=/root/config/prometheus.yml",
"--use-logs-new-schema=true"
]
# ports:
# - "6060:6060" # pprof port
# - "8080:8080" # query-service port
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
- ./data/signoz/:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
- DASHBOARDS_PATH=/root/config/dashboards
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd
restart: on-failure
healthcheck:
test:
[
"CMD",
"wget",
"--spider",
"-q",
"localhost:8080/api/v1/health"
]
interval: 30s
timeout: 5s
retries: 3
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
- alertmanager
- query-service
ports:
- "3301:3301"
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
depends_on:
clickhouse:
condition: service_healthy
# clickhouse-2:
# condition: service_healthy
# clickhouse-3:
# condition: service_healthy
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.12}
container_name: signoz-otel-collector
command:
[
"--config=/etc/otel-collector-config.yaml",
"--manager-config=/etc/manager-config.yaml",
"--copy-path=/var/tmp/collector-config.yaml",
"--feature-gates=-pkg.translator.prometheus.NormalizeName"
]
user: root # required for reading docker container logs
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- DOCKER_MULTI_NODE_CLUSTER=false
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8888:8888" # OtelCollector internal metrics
# - "8889:8889" # signoz spanmetrics exposed by the agent
# - "9411:9411" # Zipkin port
# - "13133:13133" # health check extension
# - "14250:14250" # Jaeger gRPC
# - "14268:14268" # Jaeger thrift HTTP
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zPages extension
restart: on-failure
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator:
condition: service_completed_successfully
query-service:
condition: service_healthy
logspout:
image: "gliderlabs/logspout:v3.2.14"
container_name: signoz-logspout
volumes:
- /etc/hostname:/etc/host_hostname:ro
- /var/run/docker.sock:/var/run/docker.sock
command: syslog+tcp://otel-collector:2255
depends_on:
- otel-collector
restart: on-failure

View File

@@ -1,5 +1,8 @@
version: "2.4"
include:
- test-app-docker-compose.yaml
x-clickhouse-defaults: &clickhouse-defaults
restart: on-failure
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
@@ -149,7 +152,7 @@ services:
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
container_name: signoz-alertmanager
volumes:
- ./data/alertmanager:/data
@@ -164,13 +167,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.56.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 +207,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -216,7 +219,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 +233,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.12}
container_name: signoz-otel-collector
command:
[
@@ -280,29 +283,3 @@ services:
depends_on:
- otel-collector
restart: on-failure
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: [ "all" ]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "signoz/locust:1.2.3"
container_name: load-hotrod
hostname: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../common/locust-scripts:/locust

View File

@@ -1,307 +1,3 @@
version: "2.4"
x-clickhouse-defaults: &clickhouse-defaults
restart: on-failure
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:24.1.2-alpine
tty: true
depends_on:
- zookeeper-1
# - zookeeper-2
# - zookeeper-3
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test:
[
"CMD",
"wget",
"--spider",
"-q",
"0.0.0.0:8123/ping"
]
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
x-db-depend: &db-depend
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator:
condition: service_completed_successfully
# clickhouse-2:
# condition: service_healthy
# clickhouse-3:
# condition: service_healthy
services:
zookeeper-1:
image: bitnami/zookeeper:3.7.1
container_name: signoz-zookeeper-1
hostname: zookeeper-1
user: root
ports:
- "2181:2181"
- "2888:2888"
- "3888:3888"
volumes:
- ./data/zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
# - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
# zookeeper-2:
# image: bitnami/zookeeper:3.7.0
# container_name: signoz-zookeeper-2
# hostname: zookeeper-2
# user: root
# ports:
# - "2182:2181"
# - "2889:2888"
# - "3889:3888"
# volumes:
# - ./data/zookeeper-2:/bitnami/zookeeper
# environment:
# - ZOO_SERVER_ID=2
# - ZOO_SERVERS=zookeeper-1:2888:3888,0.0.0.0:2888:3888,zookeeper-3:2888:3888
# - ALLOW_ANONYMOUS_LOGIN=yes
# - ZOO_AUTOPURGE_INTERVAL=1
# zookeeper-3:
# image: bitnami/zookeeper:3.7.0
# container_name: signoz-zookeeper-3
# hostname: zookeeper-3
# user: root
# ports:
# - "2183:2181"
# - "2890:2888"
# - "3890:3888"
# volumes:
# - ./data/zookeeper-3:/bitnami/zookeeper
# environment:
# - ZOO_SERVER_ID=3
# - ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
# - ALLOW_ANONYMOUS_LOGIN=yes
# - ZOO_AUTOPURGE_INTERVAL=1
clickhouse:
<<: *clickhouse-defaults
container_name: signoz-clickhouse
hostname: clickhouse
ports:
- "9000:9000"
- "8123:8123"
- "9181:9181"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
- ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/
- ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-2:
# <<: *clickhouse-defaults
# container_name: signoz-clickhouse-2
# hostname: clickhouse-2
# ports:
# - "9001:9000"
# - "8124:8123"
# - "9182:9181"
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-2/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-3:
# <<: *clickhouse-defaults
# container_name: signoz-clickhouse-3
# hostname: clickhouse-3
# ports:
# - "9002:9000"
# - "8125:8123"
# - "9183:9181"
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-3/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
container_name: signoz-alertmanager
volumes:
- ./data/alertmanager:/data
depends_on:
query-service:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8085
- --storage.path=/data
# 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}
container_name: signoz-query-service
command:
[
"-config=/root/config/prometheus.yml"
# "--prefer-delta=true"
]
# ports:
# - "6060:6060" # pprof port
# - "8080:8080" # query-service port
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
- ./data/signoz/:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
- DASHBOARDS_PATH=/root/config/dashboards
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd
restart: on-failure
healthcheck:
test:
[
"CMD",
"wget",
"--spider",
"-q",
"localhost:8080/api/v1/health"
]
interval: 30s
timeout: 5s
retries: 3
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
container_name: signoz-frontend
restart: on-failure
depends_on:
- alertmanager
- query-service
ports:
- "3301:3301"
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
depends_on:
clickhouse:
condition: service_healthy
# clickhouse-2:
# condition: service_healthy
# clickhouse-3:
# condition: service_healthy
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
container_name: signoz-otel-collector
command:
[
"--config=/etc/otel-collector-config.yaml",
"--manager-config=/etc/manager-config.yaml",
"--copy-path=/var/tmp/collector-config.yaml",
"--feature-gates=-pkg.translator.prometheus.NormalizeName"
]
user: root # required for reading docker container logs
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- DOCKER_MULTI_NODE_CLUSTER=false
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8888:8888" # OtelCollector internal metrics
# - "8889:8889" # signoz spanmetrics exposed by the agent
# - "9411:9411" # Zipkin port
# - "13133:13133" # health check extension
# - "14250:14250" # Jaeger gRPC
# - "14268:14268" # Jaeger thrift HTTP
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zPages extension
restart: on-failure
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator:
condition: service_completed_successfully
query-service:
condition: service_healthy
logspout:
image: "gliderlabs/logspout:v3.2.14"
container_name: signoz-logspout
volumes:
- /etc/hostname:/etc/host_hostname:ro
- /var/run/docker.sock:/var/run/docker.sock
command: syslog+tcp://otel-collector:2255
depends_on:
- otel-collector
restart: on-failure
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: [ "all" ]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "signoz/locust:1.2.3"
container_name: load-hotrod
hostname: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../common/locust-scripts:/locust
include:
- test-app-docker-compose.yaml
- docker-compose-minimal.yaml

View File

@@ -142,8 +142,8 @@ extensions:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
docker_multi_node_cluster: ${env:DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
@@ -152,8 +152,9 @@ exporters:
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
docker_multi_node_cluster: ${env:DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
use_new_schema: true
# logging: {}
service:

View File

@@ -0,0 +1,26 @@
services:
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: [ "all" ]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "signoz/locust:1.2.3"
container_name: load-hotrod
hostname: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../common/locust-scripts:/locust

View File

@@ -9,7 +9,15 @@ import (
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
if !strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+gateway.AllowedPrefix) {
validPath := false
for _, allowedPrefix := range gateway.AllowedPrefix {
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
validPath = true
break
}
}
if !validPath {
rw.WriteHeader(http.StatusNotFound)
return
}

View File

@@ -53,7 +53,11 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
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 {
// What is query.QueryName == query.Expression doing here?
// In the current implementation, the way to recognize if a query is a formula is by
// checking if the expression is the same as the query name. if the expression is different
// then it is a formula. otherwise, it is simple builder query.
if query.DataSource != v3.DataSourceMetrics && query.QueryName == query.Expression {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("all queries must have metric data source")}, nil)
return
}
@@ -100,18 +104,24 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
anomaly.WithReader[*anomaly.HourlyProvider](aH.opts.DataConnector),
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](aH.opts.FeatureFlags),
)
default:
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),
)
}
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
resp := v3.QueryRangeResponse{
Result: anomalies.Results,
ResultType: "anomaly",
}
aH.Respond(w, uniqueResults)
aH.Respond(w, resp)
} else {
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
aH.QueryRangeV4(w, r)

View File

@@ -364,6 +364,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
@@ -757,7 +758,7 @@ func makeRulesManager(
RepoURL: ruleRepoURL,
DBConn: db,
Context: context.Background(),
Logger: nil,
Logger: zap.L(),
DisableRules: disableRules,
FeatureFlags: fm,
Reader: ch,

View File

@@ -14,6 +14,8 @@ var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
var KafkaSpanEval = GetOrDefaultEnv("KAFKA_SPAN_EVAL", "false")
func GetOrDefaultEnv(key string, fallback string) string {
v := os.Getenv(key)
if len(v) == 0 {

View File

@@ -8,9 +8,9 @@ import (
"strings"
)
const (
var (
RoutePrefix string = "/api/gateway"
AllowedPrefix string = "/v1/workspaces/me"
AllowedPrefix []string = []string{"/v1/workspaces/me", "/v2/profiles/me"}
)
type proxy struct {

View File

@@ -20,6 +20,8 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
prommodel "github.com/prometheus/common/model"
zapotlpencoder "github.com/SigNoz/zap_otlp/zap_otlp_encoder"
zapotlpsync "github.com/SigNoz/zap_otlp/zap_otlp_sync"
@@ -77,6 +79,10 @@ func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
return logger
}
func init() {
prommodel.NameValidationScheme = prommodel.UTF8Validation
}
func main() {
var promConfigPath, skipTopLvlOpsPath string

View File

@@ -373,7 +373,7 @@ var EnterprisePlan = basemodel.FeatureSet{
},
basemodel.Feature{
Name: basemodel.AnomalyDetection,
Active: true,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",

View File

@@ -250,7 +250,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
}
lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel)
resultLabels := labels.NewBuilder(smpl.MetricOrig).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels()
resultLabels := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels()
for name, value := range r.Labels().Map() {
lb.Set(name, expand(value))
@@ -262,7 +262,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
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)})
annotations = append(annotations, labels.Label{Name: name, Value: expand(value)})
}
if smpl.IsMissing {
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())

View File

@@ -73,7 +73,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
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)
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, baserules.RuleTypeProm, baserules.RuleTypeThreshold)
}
return task, nil

View File

@@ -68,7 +68,7 @@
"css-loader": "5.0.0",
"css-minimizer-webpack-plugin": "5.0.1",
"dayjs": "^1.10.7",
"dompurify": "3.0.0",
"dompurify": "3.1.3",
"dotenv": "8.2.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
@@ -239,6 +239,7 @@
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1"
"phin": "^3.7.1",
"body-parser": "1.20.3"
}
}

View File

@@ -56,6 +56,7 @@
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_above_below": "above/below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
@@ -110,6 +111,8 @@
"choose_alert_type": "Choose a type for the alert",
"metric_based_alert": "Metric based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"anomaly_based_alert": "Anomaly based Alert",
"anomaly_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"log_based_alert": "Log-based Alert",
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
"traces_based_alert": "Trace-based Alert",
@@ -118,6 +121,8 @@
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for",
"text_require_min_points": "Run alert evaluation only when there are minimum of",
"text_num_points": "data points in each result group",
"text_alert_frequency": "Run alert every",
"text_for": "minutes",
"selected_query_placeholder": "Select query"

View File

@@ -43,6 +43,7 @@
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_above_below": "above/below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",

View File

@@ -13,9 +13,12 @@
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"alert_form_step1": "Choose a detection method",
"alert_form_step2": "Define the metric",
"alert_form_step3": "Define Alert Conditions",
"alert_form_step4": "Alert Configuration",
"threshold_alert_desc": "An alert is triggered whenever a metric deviates from an expected threshold.",
"anomaly_detection_alert_desc": "An alert is triggered whenever a metric deviates from an expected pattern.",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
@@ -35,6 +38,7 @@
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when",
"text_condition1_anomaly": "Send notification when the observed value for",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_1min": "1 min",
@@ -56,6 +60,7 @@
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_above_below": "above/below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
@@ -109,7 +114,9 @@
"user_tooltip_more_help": "More details on how to create alerts",
"choose_alert_type": "Choose a type for the alert",
"metric_based_alert": "Metric based Alert",
"anomaly_based_alert": "Anomaly based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"anomaly_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"log_based_alert": "Log-based Alert",
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
"traces_based_alert": "Trace-based Alert",
@@ -118,6 +125,8 @@
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for",
"text_require_min_points": "Run alert evaluation only when there are minimum of",
"text_num_points": "data points in each result group",
"text_alert_frequency": "Run alert every",
"text_for": "minutes",
"selected_query_placeholder": "Select query"

View File

@@ -43,6 +43,7 @@
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_above_below": "above/below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",

View File

@@ -59,9 +59,6 @@ function App(): JSX.Element {
const isDarkMode = useIsDarkMode();
const isOnboardingEnabled =
useFeatureFlags(FeatureKeys.ONBOARDING)?.active || false;
const isChatSupportEnabled =
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
@@ -77,6 +74,10 @@ function App(): JSX.Element {
},
});
const isOnboardingEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.ONBOARDING)?.active ||
false;
if (!isOnboardingEnabled || !isCloudUserVal) {
const newRoutes = routes.filter(
(route) => route?.path !== ROUTES.GET_STARTED,
@@ -231,6 +232,10 @@ function App(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
useEffect(() => {
console.info('We are hiring! https://jobs.gem.com/signoz');
}, []);
return (
<ConfigProvider theme={themeConfig}>
<Router history={history}>

View File

@@ -7,6 +7,7 @@ const create = async (
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.post('/rules', {
...props.data,
version: 'v4',
});
return {

View File

@@ -0,0 +1,5 @@
.client-side-qb-search {
.ant-select-selection-search {
width: max-content !important;
}
}

View File

@@ -0,0 +1,654 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './ClientSideQBSearch.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Select, Tag, Tooltip } from 'antd';
import {
OPERATORS,
QUERY_BUILDER_OPERATORS_BY_TYPES,
QUERY_BUILDER_SEARCH_VALUES,
} from 'constants/queryBuilder';
import { CustomTagProps } from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { selectStyle } from 'container/QueryBuilder/filters/QueryBuilderSearch/config';
import { PLACEHOLDER } from 'container/QueryBuilder/filters/QueryBuilderSearch/constant';
import { TypographyText } from 'container/QueryBuilder/filters/QueryBuilderSearch/style';
import {
checkCommaInValue,
getOperatorFromValue,
getOperatorValue,
getTagToken,
isInNInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import {
DropdownState,
ITag,
Option,
} from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import Suggestions from 'container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions';
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
import { validationMapper } from 'hooks/queryBuilder/useIsValidTag';
import { operatorTypeMapper } from 'hooks/queryBuilder/useOperatorType';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { isArray, isEmpty, isEqual, isObject } from 'lodash-es';
import { ChevronDown, ChevronUp } from 'lucide-react';
import type { BaseSelectRef } from 'rc-select';
import {
KeyboardEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
export interface AttributeKey {
key: string;
}
export interface AttributeValuesMap {
[key: string]: AttributeValue;
}
interface ClientSideQBSearchProps {
filters: TagFilter;
onChange: (value: TagFilter) => void;
whereClauseConfig?: WhereClauseConfig;
placeholder?: string;
className?: string;
suffixIcon?: React.ReactNode;
attributeValuesMap?: AttributeValuesMap;
attributeKeys: AttributeKey[];
}
interface AttributeValue {
stringAttributeValues: string[] | [];
numberAttributeValues: number[] | [];
boolAttributeValues: boolean[] | [];
}
function ClientSideQBSearch(
props: ClientSideQBSearchProps,
): React.ReactElement {
const {
onChange,
placeholder,
className,
suffixIcon,
whereClauseConfig,
attributeValuesMap,
attributeKeys,
filters,
} = props;
const isDarkMode = useIsDarkMode();
const selectRef = useRef<BaseSelectRef>(null);
const [isOpen, setIsOpen] = useState<boolean>(false);
// create the tags from the initial query here, this should only be computed on the first load as post that tags and query will be always in sync.
const [tags, setTags] = useState<ITag[]>(filters.items as ITag[]);
// this will maintain the current state of in process filter item
const [currentFilterItem, setCurrentFilterItem] = useState<ITag | undefined>();
const [currentState, setCurrentState] = useState<DropdownState>(
DropdownState.ATTRIBUTE_KEY,
);
// to maintain the current running state until the tokenization happens for the tag
const [searchValue, setSearchValue] = useState<string>('');
const [dropdownOptions, setDropdownOptions] = useState<Option[]>([]);
const attributeValues = useMemo(() => {
if (currentFilterItem?.key?.key) {
return attributeValuesMap?.[currentFilterItem.key.key];
}
return {
stringAttributeValues: [],
numberAttributeValues: [],
boolAttributeValues: [],
};
}, [attributeValuesMap, currentFilterItem?.key?.key]);
const handleDropdownSelect = useCallback(
(value: string) => {
let parsedValue: BaseAutocompleteData | string;
try {
parsedValue = JSON.parse(value);
} catch {
parsedValue = value;
}
if (currentState === DropdownState.ATTRIBUTE_KEY) {
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) {
setTags((prev) => [
...prev,
{
key: currentFilterItem?.key,
op: value,
value: '',
} as ITag,
]);
setCurrentFilterItem(undefined);
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else {
setCurrentFilterItem((prev) => ({
key: prev?.key as BaseAutocompleteData,
op: value as string,
value: '',
}));
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
setSearchValue(`${currentFilterItem?.key?.key} ${value}`);
}
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
const operatorType =
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY;
if (isMulti) {
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
// this condition takes care of adding the IN/NIN multi values when pressed enter on an already existing value.
// not the best interaction but in sync with what we have today!
if (tagValue.includes(String(value))) {
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
setCurrentFilterItem(undefined);
setTags((prev) => [
...prev,
{
key: currentFilterItem?.key,
op: currentFilterItem?.op,
value: tagValue,
} as ITag,
]);
return;
}
// this is for adding subsequent comma seperated values
const newSearch = [...tagValue];
newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value;
const newSearchValue = newSearch.join(',');
setSearchValue(`${tagKey} ${tagOperator} ${newSearchValue},`);
} else {
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
setCurrentFilterItem(undefined);
setTags((prev) => [
...prev,
{
key: currentFilterItem?.key,
op: currentFilterItem?.op,
value,
} as ITag,
]);
}
}
},
[currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue],
);
const handleSearch = useCallback((value: string) => {
setSearchValue(value);
}, []);
const onInputKeyDownHandler = useCallback(
(event: KeyboardEvent<Element>): void => {
if (event.key === 'Backspace' && !searchValue) {
event.stopPropagation();
setTags((prev) => prev.slice(0, -1));
}
},
[searchValue],
);
const handleOnBlur = useCallback((): void => {
if (searchValue) {
const operatorType =
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
// if key is added and operator is not present then convert to body CONTAINS key
if (
currentFilterItem?.key &&
isEmpty(currentFilterItem?.op) &&
whereClauseConfig?.customKey === 'body' &&
whereClauseConfig?.customOp === OPERATORS.CONTAINS
) {
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 (
currentFilterItem?.op === OPERATORS.EXISTS ||
currentFilterItem?.op === OPERATORS.NOT_EXISTS
) {
// is exists and not exists operator is present then convert directly to tag! no need of value here
setTags((prev) => [
...prev,
{
key: currentFilterItem?.key,
op: currentFilterItem?.op,
value: '',
},
]);
setCurrentFilterItem(undefined);
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else if (
// if the current state is in sync with the kind of operator used then convert into a tag
validationMapper[operatorType]?.(
isArray(currentFilterItem?.value)
? currentFilterItem?.value.length || 0
: 1,
)
) {
setTags((prev) => [
...prev,
{
key: currentFilterItem?.key as BaseAutocompleteData,
op: currentFilterItem?.op as string,
value: currentFilterItem?.value || '',
},
]);
setCurrentFilterItem(undefined);
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
}
}
}, [
currentFilterItem?.key,
currentFilterItem?.op,
currentFilterItem?.value,
searchValue,
whereClauseConfig?.customKey,
whereClauseConfig?.customOp,
]);
// this useEffect takes care of tokenisation based on the search state
useEffect(() => {
// if there is no search value reset to the default state
if (!searchValue) {
setCurrentFilterItem(undefined);
setCurrentState(DropdownState.ATTRIBUTE_KEY);
}
// split the current search value based on delimiters
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
if (
// Case 1 - if key is defined but the search text doesn't match with the set key,
// can happen when user selects from dropdown and then deletes a few characters
currentFilterItem?.key &&
currentFilterItem?.key?.key !== tagKey.split(' ')[0]
) {
setCurrentFilterItem(undefined);
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else if (tagOperator && isEmpty(currentFilterItem?.op)) {
// Case 2 -> key is set and now typing for the operator
if (
tagOperator === OPERATORS.EXISTS ||
tagOperator === OPERATORS.NOT_EXISTS
) {
setTags((prev) => [
...prev,
{
key: currentFilterItem?.key,
op: tagOperator,
value: '',
} as ITag,
]);
setCurrentFilterItem(undefined);
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else {
setCurrentFilterItem((prev) => ({
key: prev?.key as BaseAutocompleteData,
op: tagOperator,
value: '',
}));
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
}
} else if (
// Case 3 -> selected operator from dropdown and then erased a part of it
!isEmpty(currentFilterItem?.op) &&
tagOperator !== currentFilterItem?.op
) {
setCurrentFilterItem((prev) => ({
key: prev?.key as BaseAutocompleteData,
op: '',
value: '',
}));
setCurrentState(DropdownState.OPERATOR);
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
// Case 4 -> the final value state where we set the current filter values and the tokenisation happens on either
// dropdown click or blur event
const currentValue = {
key: currentFilterItem?.key as BaseAutocompleteData,
op: currentFilterItem?.op as string,
value: tagValue,
};
if (!isEqual(currentValue, currentFilterItem)) {
setCurrentFilterItem((prev) => ({
key: prev?.key as BaseAutocompleteData,
op: prev?.op as string,
value: tagValue,
}));
}
}
}, [
currentFilterItem,
currentFilterItem?.key,
currentFilterItem?.op,
searchValue,
currentState,
]);
// the useEffect takes care of setting the dropdown values correctly on change of the current state
useEffect(() => {
if (currentState === DropdownState.ATTRIBUTE_KEY) {
const filteredAttributeKeys = attributeKeys.filter((key) =>
key.key.startsWith(searchValue),
);
setDropdownOptions(
filteredAttributeKeys?.map(
(key) =>
({
label: key.key,
value: key,
} as Option),
) || [],
);
}
if (currentState === DropdownState.OPERATOR) {
const keyOperator = searchValue.split(' ');
const partialOperator = keyOperator?.[1];
const strippedKey = keyOperator?.[0];
let operatorOptions;
if (currentFilterItem?.key?.dataType) {
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES[
currentFilterItem.key
.dataType as keyof typeof QUERY_BUILDER_OPERATORS_BY_TYPES
].map((operator) => ({
label: operator,
value: operator,
}));
if (partialOperator) {
operatorOptions = operatorOptions.filter((op) =>
op.label.startsWith(partialOperator.toLocaleUpperCase()),
);
}
setDropdownOptions(operatorOptions);
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
label: operator,
value: operator,
}));
setDropdownOptions(operatorOptions);
} else {
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
(operator) => ({
label: operator,
value: operator,
}),
);
if (partialOperator) {
operatorOptions = operatorOptions.filter((op) =>
op.label.startsWith(partialOperator.toLocaleUpperCase()),
);
}
setDropdownOptions(operatorOptions);
}
}
if (currentState === DropdownState.ATTRIBUTE_VALUE) {
const values: Array<string | number | boolean> = [];
const { tagValue } = getTagToken(searchValue);
if (isArray(tagValue)) {
if (!isEmpty(tagValue[tagValue.length - 1]))
values.push(tagValue[tagValue.length - 1]);
} else if (!isEmpty(tagValue)) values.push(tagValue);
const currentAttributeValues =
attributeValues?.stringAttributeValues ||
attributeValues?.numberAttributeValues ||
attributeValues?.boolAttributeValues ||
[];
values.push(...currentAttributeValues);
if (attributeValuesMap) {
setDropdownOptions(
values.map(
(val) =>
({
label: checkCommaInValue(String(val)),
value: val,
} as Option),
),
);
} else {
// If attributeValuesMap is not provided, don't set dropdown options
setDropdownOptions([]);
}
}
}, [
attributeValues,
currentFilterItem?.key?.dataType,
currentState,
attributeKeys,
searchValue,
attributeValuesMap,
]);
useEffect(() => {
const filterTags: IBuilderQuery['filters'] = {
op: 'AND',
items: [],
};
tags.forEach((tag) => {
const computedTagValue =
tag.value &&
Array.isArray(tag.value) &&
tag.value[tag.value.length - 1] === ''
? tag.value?.slice(0, -1)
: tag.value ?? '';
filterTags.items.push({
id: tag.id || uuid().slice(0, 8),
key: tag.key,
op: getOperatorValue(tag.op),
value: computedTagValue,
});
});
if (!isEqual(filters, filterTags)) {
onChange(filterTags);
setTags(
filterTags.items.map((tag) => ({
...tag,
op: getOperatorFromValue(tag.op),
})) as ITag[],
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tags]);
const queryTags = useMemo(
() => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`),
[tags],
);
const onTagRender = ({
value,
closable,
onClose,
}: CustomTagProps): React.ReactElement => {
const { tagOperator } = getTagToken(value);
const isInNin = isInNInOperator(tagOperator);
const chipValue = isInNin
? value?.trim()?.replace(/,\s*$/, '')
: value?.trim();
const indexInQueryTags = queryTags.findIndex((qTag) => isEqual(qTag, value));
const tagDetails = tags[indexInQueryTags];
const onCloseHandler = (): void => {
onClose();
setSearchValue('');
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
};
const tagEditHandler = (value: string): void => {
setCurrentFilterItem(tagDetails);
setSearchValue(value);
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
};
const isDisabled = !!searchValue;
return (
<span className="qb-search-bar-tokenised-tags">
<Tag
closable={!searchValue && closable}
onClose={onCloseHandler}
className={tagDetails?.key?.type || ''}
>
<Tooltip title={chipValue}>
<TypographyText
ellipsis
$isInNin={isInNin}
disabled={isDisabled}
$isEnabled={!!searchValue}
onClick={(): void => {
if (!isDisabled) tagEditHandler(value);
}}
>
{chipValue}
</TypographyText>
</Tooltip>
</Tag>
</span>
);
};
const suffixIconContent = useMemo(() => {
if (suffixIcon) {
return suffixIcon;
}
return isOpen ? (
<ChevronUp
size={14}
color={isDarkMode ? Color.TEXT_VANILLA_100 : Color.TEXT_INK_100}
/>
) : (
<ChevronDown
size={14}
color={isDarkMode ? Color.TEXT_VANILLA_100 : Color.TEXT_INK_100}
/>
);
}, [isDarkMode, isOpen, suffixIcon]);
return (
<div className="query-builder-search-v2 ">
<Select
ref={selectRef}
getPopupContainer={popupContainer}
virtual={false}
showSearch
tagRender={onTagRender}
transitionName=""
choiceTransitionName=""
filterOption={false}
open={isOpen}
suffixIcon={suffixIconContent}
onDropdownVisibleChange={setIsOpen}
autoClearSearchValue={false}
mode="multiple"
placeholder={placeholder}
value={queryTags}
searchValue={searchValue}
className={className}
rootClassName="query-builder-search client-side-qb-search"
disabled={!attributeKeys.length}
style={selectStyle}
onSearch={handleSearch}
onSelect={handleDropdownSelect}
onInputKeyDown={onInputKeyDownHandler}
notFoundContent={null}
showAction={['focus']}
onBlur={handleOnBlur}
>
{dropdownOptions.map((option) => {
let val = option.value;
try {
if (isObject(option.value)) {
val = JSON.stringify(option.value);
} else {
val = option.value;
}
} catch {
val = option.value;
}
return (
<Select.Option key={isObject(val) ? `select-option` : val} value={val}>
<Suggestions
label={option.label}
value={option.value}
option={currentState}
searchValue={searchValue}
/>
</Select.Option>
);
})}
</Select>
</div>
);
}
ClientSideQBSearch.defaultProps = {
placeholder: PLACEHOLDER,
className: '',
suffixIcon: null,
whereClauseConfig: {},
attributeValuesMap: {},
};
export default ClientSideQBSearch;

View File

@@ -22,7 +22,13 @@ export type GetViewDetailsUsingViewKey = (
viewKey: string,
data: ViewProps[] | undefined,
) =>
| { query: Query; name: string; uuid: string; panelType: PANEL_TYPES }
| {
query: Query;
name: string;
uuid: string;
panelType: PANEL_TYPES;
extraData?: string;
}
| undefined;
export interface IsQueryUpdatedInViewProps {

View File

@@ -29,9 +29,9 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
) => {
const selectedView = data?.find((view) => view.uuid === viewKey);
if (selectedView) {
const { compositeQuery, name, uuid } = selectedView;
const { compositeQuery, name, uuid, extraData } = selectedView;
const query = mapQueryDataFromApi(compositeQuery);
return { query, name, uuid, panelType: compositeQuery.panelType };
return { query, name, uuid, panelType: compositeQuery.panelType, extraData };
}
return undefined;
};

View File

@@ -107,6 +107,7 @@ function DynamicColumnTable({
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
data-testid="additional-filters-button"
/>
</Dropdown>
)}

View File

@@ -2,6 +2,7 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { DataSource } from 'types/common/queryBuilder';
export const ALERTS_DATA_SOURCE_MAP: Record<AlertTypes, DataSource> = {
[AlertTypes.ANOMALY_BASED_ALERT]: DataSource.METRICS,
[AlertTypes.METRICS_BASED_ALERT]: DataSource.METRICS,
[AlertTypes.LOGS_BASED_ALERT]: DataSource.LOGS,
[AlertTypes.TRACES_BASED_ALERT]: DataSource.TRACES,

View File

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

View File

@@ -36,4 +36,5 @@ export enum QueryParams {
topic = 'topic',
partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery',
ruleType = 'ruleType',
}

View File

@@ -3,6 +3,10 @@ import { QueryFunctionsTypes } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
{
value: QueryFunctionsTypes.ANOMALY,
label: 'Anomaly',
},
{
value: QueryFunctionsTypes.CUTOFF_MIN,
label: 'Cut Off Min',
@@ -67,6 +71,10 @@ export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
value: QueryFunctionsTypes.TIME_SHIFT,
label: 'Time Shift',
},
{
value: QueryFunctionsTypes.TIME_SHIFT,
label: 'Time Shift',
},
];
export const logsQueryFunctionOptions: SelectOption<string, string>[] = [
@@ -80,10 +88,15 @@ interface QueryFunctionConfigType {
showInput: boolean;
inputType?: string;
placeholder?: string;
disabled?: boolean;
};
}
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
anomaly: {
showInput: false,
disabled: true,
},
cutOffMin: {
showInput: true,
inputType: 'text',

View File

@@ -1,10 +1,15 @@
import { Color } from '@signozhq/design-tokens';
import Uplot from 'components/Uplot';
import { QueryParams } from 'constants/query';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
import { useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AlertRuleTimelineGraphResponse } from 'types/api/alerts/def';
import uPlot, { AlignedData } from 'uplot';
@@ -41,11 +46,13 @@ function HorizontalTimelineGraph({
return [timestamps, states];
}, [data]);
const urlQuery = useUrlQuery();
const dispatch = useDispatch();
const options: uPlot.Options = useMemo(
() => ({
width,
height: 85,
cursor: { show: false },
axes: [
{
@@ -66,6 +73,40 @@ function HorizontalTimelineGraph({
label: 'States',
},
],
hooks: {
setSelect: [
(self): void => {
const selection = self.select;
if (selection) {
const startTime = self.posToVal(selection.left, 'x');
const endTime = self.posToVal(selection.left + selection.width, 'x');
const diff = endTime - startTime;
if (diff > 0) {
if (urlQuery.has(QueryParams.relativeTime)) {
urlQuery.delete(QueryParams.relativeTime);
}
const startTimestamp = Math.floor(startTime * 1000);
const endTimestamp = Math.floor(endTime * 1000);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
history.push({
search: urlQuery.toString(),
});
}
}
},
],
},
plugins:
transformedData?.length > 1
? [
@@ -76,7 +117,7 @@ function HorizontalTimelineGraph({
]
: [],
}),
[width, isDarkMode, transformedData],
[width, isDarkMode, transformedData.length, urlQuery, dispatch],
);
return <Uplot data={transformedData} options={options} />;
}

View File

@@ -109,8 +109,8 @@
}
.alert-rule {
&-value,
&-created-at {
&__value,
&__created-at {
color: var(--text-ink-400);
}
}

View File

@@ -1,16 +1,20 @@
import './Table.styles.scss';
import { Table } from 'antd';
import { initialFilters } from 'constants/queryBuilder';
import {
useGetAlertRuleDetailsTimelineTable,
useTimelineTable,
} from 'pages/AlertDetails/hooks';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { timelineTableColumns } from './useTimelineTable';
function TimelineTable(): JSX.Element {
const [filters, setFilters] = useState<TagFilter>(initialFilters);
const {
isLoading,
isRefetching,
@@ -18,13 +22,14 @@ function TimelineTable(): JSX.Element {
data,
isValidRuleId,
ruleId,
} = useGetAlertRuleDetailsTimelineTable();
} = useGetAlertRuleDetailsTimelineTable({ filters });
const { timelineData, totalItems } = useMemo(() => {
const { timelineData, totalItems, labels } = useMemo(() => {
const response = data?.payload?.data;
return {
timelineData: response?.items,
totalItems: response?.total,
labels: response?.labels,
};
}, [data?.payload?.data]);
@@ -42,7 +47,11 @@ function TimelineTable(): JSX.Element {
<div className="timeline-table">
<Table
rowKey={(row): string => `${row.fingerprint}-${row.value}-${row.unixMilli}`}
columns={timelineTableColumns()}
columns={timelineTableColumns({
filters,
labels: labels ?? {},
setFilters,
})}
dataSource={timelineData}
pagination={paginationConfig}
size="middle"

View File

@@ -1,13 +1,84 @@
import { EllipsisOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { ColumnsType } from 'antd/es/table';
import ClientSideQBSearch, {
AttributeKey,
} from 'components/ClientSideQBSearch/ClientSideQBSearch';
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Search } from 'lucide-react';
import AlertLabels, {
AlertLabelsProps,
} from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
import { useMemo } from 'react';
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { formatEpochTimestamp } from 'utils/timeUtils';
export const timelineTableColumns = (): ColumnsType<AlertRuleTimelineTableResponse> => [
const transformLabelsToQbKeys = (
labels: AlertRuleTimelineTableResponse['labels'],
): AttributeKey[] => Object.keys(labels).flatMap((key) => [{ key }]);
function LabelFilter({
filters,
setFilters,
labels,
}: {
setFilters: (filters: TagFilter) => void;
filters: TagFilter;
labels: AlertLabelsProps['labels'];
}): JSX.Element | null {
const isDarkMode = useIsDarkMode();
const { transformedKeys, attributesMap } = useMemo(
() => ({
transformedKeys: transformLabelsToQbKeys(labels || {}),
attributesMap: transformKeyValuesToAttributeValuesMap(labels),
}),
[labels],
);
const handleSearch = (tagFilters: TagFilter): void => {
const tagFiltersLength = tagFilters.items.length;
if (
(!tagFiltersLength && (!filters || !filters.items.length)) ||
tagFiltersLength === filters?.items.length
) {
return;
}
setFilters(tagFilters);
};
return (
<ClientSideQBSearch
onChange={handleSearch}
filters={filters}
className="alert-history-label-search"
attributeKeys={transformedKeys}
attributeValuesMap={attributesMap}
suffixIcon={
<Search
size={14}
color={isDarkMode ? Color.TEXT_VANILLA_100 : Color.TEXT_INK_100}
/>
}
/>
);
}
export const timelineTableColumns = ({
filters,
labels,
setFilters,
}: {
filters: TagFilter;
labels: AlertLabelsProps['labels'];
setFilters: (filters: TagFilter) => void;
}): ColumnsType<AlertRuleTimelineTableResponse> => [
{
title: 'STATE',
dataIndex: 'state',
@@ -20,7 +91,9 @@ export const timelineTableColumns = (): ColumnsType<AlertRuleTimelineTableRespon
),
},
{
title: 'LABELS',
title: (
<LabelFilter setFilters={setFilters} filters={filters} labels={labels} />
),
dataIndex: 'labels',
render: (labels): JSX.Element => (
<div className="alert-rule-labels">

View File

@@ -0,0 +1,110 @@
.anomaly-alert-evaluation-view {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
width: 100%;
height: 100%;
.anomaly-alert-evaluation-view-chart-section {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
&.has-multi-series-data {
width: calc(100% - 240px);
}
.anomaly-alert-evaluation-view-no-data-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.anomaly-alert-evaluation-view-series-selection {
display: flex;
flex-direction: column;
gap: 8px;
width: 240px;
padding: 0px 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list-search {
margin-bottom: 16px;
}
.anomaly-alert-evaluation-view-series-list-title {
margin-top: 12px;
font-size: 13px !important;
font-weight: 400;
}
.anomaly-alert-evaluation-view-series-list-items {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
overflow-y: auto;
.anomaly-alert-evaluation-view-series-list-item {
display: flex;
flex-direction: row;
gap: 8px;
cursor: pointer;
}
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
}
.uplot {
.u-title {
text-align: center;
font-size: 18px;
font-weight: 400;
display: flex;
height: 40px;
font-size: 13px;
align-items: center;
}
.u-legend {
display: flex;
margin-top: 16px;
tbody {
width: 100%;
.u-series {
display: inline-flex;
}
}
}
}
}

View File

@@ -0,0 +1,315 @@
import 'uplot/dist/uPlot.min.css';
import './AnomalyAlertEvaluationView.styles.scss';
import { Checkbox, Typography } from 'antd';
import Search from 'antd/es/input/Search';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useResizeObserver } from 'hooks/useDimensions';
import getAxes from 'lib/uPlotLib/utils/getAxes';
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
import { LineChart } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import uPlot from 'uplot';
function UplotChart({
data,
options,
chartRef,
}: {
data: any;
options: any;
chartRef: any;
}): JSX.Element {
const plotInstance = useRef(null);
useEffect(() => {
if (plotInstance.current) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
plotInstance.current.destroy();
}
if (data && data.length > 0) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line new-cap
plotInstance.current = new uPlot(options, data, chartRef.current);
}
return (): void => {
if (plotInstance.current) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
plotInstance.current.destroy();
}
};
}, [data, options, chartRef]);
return <div ref={chartRef} />;
}
function AnomalyAlertEvaluationView({
data,
yAxisUnit,
}: {
data: any;
yAxisUnit: string;
}): JSX.Element {
const { spline } = uPlot.paths;
// eslint-disable-next-line @typescript-eslint/naming-convention
const _spline = spline ? spline() : undefined;
const chartRef = useRef<HTMLDivElement>(null);
const isDarkMode = useIsDarkMode();
const [seriesData, setSeriesData] = useState<any>({});
const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
const [filteredSeriesKeys, setFilteredSeriesKeys] = useState<string[]>([]);
const [allSeries, setAllSeries] = useState<string[]>([]);
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
useEffect(() => {
const chartData = getUplotChartDataForAnomalyDetection(data);
setSeriesData(chartData);
setAllSeries(Object.keys(chartData));
setFilteredSeriesKeys(Object.keys(chartData));
}, [data]);
useEffect(() => {
const seriesKeys = Object.keys(seriesData);
if (seriesKeys.length === 1) {
setSelectedSeries(seriesKeys[0]); // Automatically select if only one series
} else {
setSelectedSeries(null); // Default to "Show All" if multiple series
}
}, [seriesData]);
const handleSeriesChange = (series: string | null): void => {
setSelectedSeries(series);
};
const bandsPlugin = {
hooks: {
draw: [
(u: any): void => {
if (!selectedSeries) return;
const { ctx } = u;
const upperBandIdx = 3;
const lowerBandIdx = 4;
const xData = u.data[0];
const yUpperData = u.data[upperBandIdx];
const yLowerData = u.data[lowerBandIdx];
const strokeStyle =
u.series[1]?.stroke || seriesData[selectedSeries].color;
const fillStyle =
typeof strokeStyle === 'string'
? strokeStyle.replace(')', ', 0.1)')
: 'rgba(255, 255, 255, 0.1)';
ctx.beginPath();
const firstX = u.valToPos(xData[0], 'x', true);
const firstUpperY = u.valToPos(yUpperData[0], 'y', true);
ctx.moveTo(firstX, firstUpperY);
for (let i = 0; i < xData.length; i++) {
const x = u.valToPos(xData[i], 'x', true);
const y = u.valToPos(yUpperData[i], 'y', true);
ctx.lineTo(x, y);
}
for (let i = xData.length - 1; i >= 0; i--) {
const x = u.valToPos(xData[i], 'x', true);
const y = u.valToPos(yLowerData[i], 'y', true);
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
},
],
},
};
const initialData = allSeries.length
? [
seriesData[allSeries[0]].data[0], // Shared X-axis
...allSeries.map((key) => seriesData[key].data[1]), // Map through Y-axis data for all series
]
: [];
const options = {
width: dimensions.width,
height: dimensions.height - 36,
plugins: [bandsPlugin],
focus: {
alpha: 0.3,
},
series: [
{
label: 'Time',
},
...(selectedSeries
? [
{
label: `Main Series`,
stroke: seriesData[selectedSeries].color,
width: 2,
show: true,
paths: _spline,
},
{
label: `Predicted Value`,
stroke: seriesData[selectedSeries].color,
width: 1,
dash: [2, 2],
show: true,
paths: _spline,
},
{
label: `Upper Band`,
stroke: 'transparent',
show: false,
paths: _spline,
},
{
label: `Lower Band`,
stroke: 'transparent',
show: false,
paths: _spline,
},
]
: allSeries.map((seriesKey) => ({
label: seriesKey,
stroke: seriesData[seriesKey].color,
width: 2,
show: true,
paths: _spline,
}))),
],
scales: {
x: {
time: true,
},
y: {
...getYAxisScaleForAnomalyDetection({
seriesData,
selectedSeries,
initialData,
yAxisUnit,
}),
},
},
grid: {
show: true,
},
legend: {
show: true,
},
axes: getAxes(isDarkMode, yAxisUnit),
};
const handleSearch = (searchText: string): void => {
if (!searchText || searchText.length === 0) {
setFilteredSeriesKeys(allSeries);
return;
}
const filteredSeries = allSeries.filter((series) =>
series.toLowerCase().includes(searchText.toLowerCase()),
);
setFilteredSeriesKeys(filteredSeries);
};
const handleSearchValueChange = useDebouncedFn((event): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const value = event?.target?.value || '';
handleSearch(value);
}, 300);
return (
<div className="anomaly-alert-evaluation-view">
<div
className={`anomaly-alert-evaluation-view-chart-section ${
allSeries.length > 1 ? 'has-multi-series-data' : ''
}`}
ref={graphRef}
>
{allSeries.length > 0 ? (
<UplotChart
data={selectedSeries ? seriesData[selectedSeries].data : initialData}
options={options}
chartRef={chartRef}
/>
) : (
<div className="anomaly-alert-evaluation-view-no-data-container">
<LineChart size={48} strokeWidth={0.5} />
<Typography>No Data</Typography>
</div>
)}
</div>
{allSeries.length > 1 && (
<div className="anomaly-alert-evaluation-view-series-selection">
{allSeries.length > 1 && (
<div className="anomaly-alert-evaluation-view-series-list">
<Search
className="anomaly-alert-evaluation-view-series-list-search"
placeholder="Search a series"
allowClear
onChange={handleSearchValueChange}
/>
<div className="anomaly-alert-evaluation-view-series-list-items">
{filteredSeriesKeys.length > 0 && (
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
type="checkbox"
name="series"
value="all"
checked={selectedSeries === null}
onChange={(): void => handleSeriesChange(null)}
>
Show All
</Checkbox>
)}
{filteredSeriesKeys.map((seriesKey) => (
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
key={seriesKey}
type="checkbox"
name="series"
value={seriesKey}
checked={selectedSeries === seriesKey}
onChange={(): void => handleSeriesChange(seriesKey)}
>
{seriesKey}
</Checkbox>
))}
{filteredSeriesKeys.length === 0 && (
<Typography>No series found</Typography>
)}
</div>
</div>
)}
</div>
)}
</div>
);
}
export default AnomalyAlertEvaluationView;

View File

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

View File

@@ -232,19 +232,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
const isDashboardView = (): boolean => {
/**
* need to match using regex here as the getRoute function will not work for
* routes with id
*/
const regex = /^\/dashboard\/[a-zA-Z0-9_-]+$/;
return regex.test(pathname);
};
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
const isDashboardWidgetView = (): boolean => {
const regex = /^\/dashboard\/[a-zA-Z0-9_-]+\/new$/;
return regex.test(pathname);
};
const isDashboardView = (): boolean =>
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+$/);
const isDashboardWidgetView = (): boolean =>
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+\/new$/);
const isTraceDetailsView = (): boolean =>
isPathMatch(/^\/trace\/[a-zA-Z0-9]+(\?.*)?$/);
useEffect(() => {
if (isDarkMode) {
@@ -304,6 +301,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isMessagingQueues()
? 0
: '0 1rem',
...(isTraceDetailsView() ? { marginRight: 0 } : {}),
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}

View File

@@ -58,6 +58,21 @@ const calculateStartEndTime = (
export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
const { data, billAmount } = props;
// Added this to fix the issue where breakdown with one day data are causing the bars to spread across multiple days
data?.details?.breakdown?.forEach((breakdown: any) => {
if (breakdown?.dayWiseBreakdown?.breakdown?.length === 1) {
const currentDay = breakdown.dayWiseBreakdown.breakdown[0];
const nextDay = {
...currentDay,
timestamp: currentDay.timestamp + 86400,
count: 0,
size: 0,
quantity: 0,
total: 0,
};
breakdown.dayWiseBreakdown.breakdown.push(nextDay);
}
});
const graphCompatibleData = useMemo(
() => convertDataToMetricRangePayload(data),
[data],

View File

@@ -3,25 +3,41 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { OptionType } from './types';
export const getOptionList = (t: TFunction): OptionType[] => [
{
title: t('metric_based_alert'),
selection: AlertTypes.METRICS_BASED_ALERT,
description: t('metric_based_alert_desc'),
},
{
title: t('log_based_alert'),
selection: AlertTypes.LOGS_BASED_ALERT,
description: t('log_based_alert_desc'),
},
{
title: t('traces_based_alert'),
selection: AlertTypes.TRACES_BASED_ALERT,
description: t('traces_based_alert_desc'),
},
{
title: t('exceptions_based_alert'),
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
description: t('exceptions_based_alert_desc'),
},
];
export const getOptionList = (
t: TFunction,
isAnomalyDetectionEnabled: boolean,
): OptionType[] => {
const optionList: OptionType[] = [
{
title: t('metric_based_alert'),
selection: AlertTypes.METRICS_BASED_ALERT,
description: t('metric_based_alert_desc'),
},
{
title: t('log_based_alert'),
selection: AlertTypes.LOGS_BASED_ALERT,
description: t('log_based_alert_desc'),
},
{
title: t('traces_based_alert'),
selection: AlertTypes.TRACES_BASED_ALERT,
description: t('traces_based_alert_desc'),
},
{
title: t('exceptions_based_alert'),
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
description: t('exceptions_based_alert_desc'),
},
];
if (isAnomalyDetectionEnabled) {
optionList.unshift({
title: t('anomaly_based_alert'),
selection: AlertTypes.ANOMALY_BASED_ALERT,
description: t('anomaly_based_alert_desc'),
isBeta: true,
});
}
return optionList;
};

View File

@@ -1,6 +1,8 @@
import { Row, Typography } from 'antd';
import { Row, Tag, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -12,11 +14,18 @@ import { OptionType } from './types';
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
const { t } = useTranslation(['alerts']);
const optionList = getOptionList(t);
const isAnomalyDetectionEnabled =
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
const optionList = getOptionList(t, isAnomalyDetectionEnabled);
function handleRedirection(option: AlertTypes): void {
let url = '';
switch (option) {
case AlertTypes.ANOMALY_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
break;
case AlertTypes.METRICS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
@@ -52,9 +61,17 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
<AlertTypeCard
key={option.selection}
title={option.title}
extra={
option.isBeta ? (
<Tag bordered={false} color="geekblue">
Beta
</Tag>
) : undefined
}
onClick={(): void => {
onSelect(option.selection);
}}
data-testid={`alert-type-card-${option.selection}`}
>
{option.description}{' '}
<Typography.Link

View File

@@ -4,4 +4,5 @@ export interface OptionType {
title: string;
selection: AlertTypes;
description: string;
isBeta?: boolean;
}

View File

@@ -7,9 +7,11 @@ import {
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
AlertDef,
defaultAlgorithm,
defaultCompareOp,
defaultEvalWindow,
defaultMatchType,
defaultSeasonality,
} from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
@@ -46,6 +48,8 @@ export const alertDefaults: AlertDef = {
},
op: defaultCompareOp,
matchType: defaultMatchType,
algorithm: defaultAlgorithm,
seasonality: defaultSeasonality,
},
labels: {
severity: 'warning',
@@ -145,6 +149,7 @@ export const exceptionAlertDefaults: AlertDef = {
};
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
[AlertTypes.ANOMALY_BASED_ALERT]: alertDefaults,
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,

View File

@@ -2,7 +2,7 @@ import { Form, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import FormAlertRules from 'container/FormAlertRules';
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import history from 'lib/history';
import { useEffect, useState } from 'react';
@@ -45,6 +45,7 @@ function CreateRules(): JSX.Element {
const onSelectType = (typ: AlertTypes): void => {
setAlertType(typ);
switch (typ) {
case AlertTypes.LOGS_BASED_ALERT:
setInitValues(logAlertDefaults);
@@ -55,13 +56,37 @@ function CreateRules(): JSX.Element {
case AlertTypes.EXCEPTIONS_BASED_ALERT:
setInitValues(exceptionAlertDefaults);
break;
case AlertTypes.ANOMALY_BASED_ALERT:
setInitValues({
...alertDefaults,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
});
break;
default:
setInitValues({
...alertDefaults,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
});
}
queryParams.set(QueryParams.alertType, typ);
queryParams.set(
QueryParams.alertType,
typ === AlertTypes.ANOMALY_BASED_ALERT
? AlertTypes.METRICS_BASED_ALERT
: typ,
);
if (typ === AlertTypes.ANOMALY_BASED_ALERT) {
queryParams.set(
QueryParams.ruleType,
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
);
} else {
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
}
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
history.replace(generatedUrl);
};

View File

@@ -7,18 +7,16 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
const [formInstance] = Form.useForm();
return (
<div style={{ marginTop: '1rem' }}>
<FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance}
initialValue={initialValue}
ruleId={ruleId}
/>
</div>
<FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance}
initialValue={initialValue}
ruleId={ruleId}
/>
);
}

View File

@@ -24,6 +24,9 @@ import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
import { useOptionsMenu } from 'container/OptionsMenu';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
@@ -34,7 +37,7 @@ import useErrorNotification from 'hooks/useErrorNotification';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { cloneDeep } from 'lodash-es';
import { cloneDeep, isEqual } from 'lodash-es';
import {
Check,
ConciergeBell,
@@ -58,7 +61,9 @@ import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
@@ -252,6 +257,46 @@ function ExplorerOptions({
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { options, handleOptionsChange } = useOptionsMenu({
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
dataSource: DataSource.TRACES,
aggregateOperator: StringOperators.NOOP,
});
type ExtraData = {
selectColumns?: BaseAutocompleteData[];
};
const updateOrRestoreSelectColumns = (
key: string,
allViewsData: ViewProps[] | undefined,
options: OptionsQuery,
handleOptionsChange: (newQueryData: OptionsQuery) => void,
): void => {
const currentViewDetails = getViewDetailsUsingViewKey(key, allViewsData);
if (!currentViewDetails) {
return;
}
let extraData: ExtraData = {};
try {
extraData = JSON.parse(currentViewDetails?.extraData ?? '{}') as ExtraData;
} catch (error) {
console.error('Error parsing extraData:', error);
}
if (extraData.selectColumns?.length) {
handleOptionsChange({
...options,
selectColumns: extraData.selectColumns,
});
} else if (!isEqual(defaultTraceSelectedColumns, options.selectColumns)) {
handleOptionsChange({
...options,
selectColumns: defaultTraceSelectedColumns,
});
}
};
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
const currentViewDetails = getViewDetailsUsingViewKey(
@@ -321,6 +366,13 @@ function ExplorerOptions({
updatePreservedViewInLocalStorage(option);
updateOrRestoreSelectColumns(
option.key,
viewsData?.data?.data,
options,
handleOptionsChange,
);
if (ref.current) {
ref.current.blur();
}
@@ -360,14 +412,20 @@ function ExplorerOptions({
viewName: newViewName || '',
compositeQuery,
sourcePage: sourcepage,
extraData: JSON.stringify({ color }),
extraData: JSON.stringify({
color,
selectColumns: options.selectColumns,
}),
});
const onSaveHandler = (): void => {
saveNewViewHandler({
compositeQuery,
handlePopOverClose: hideSaveViewModal,
extraData: JSON.stringify({ color }),
extraData: JSON.stringify({
color,
selectColumns: options.selectColumns,
}),
notifications,
panelType: panelType || PANEL_TYPES.LIST,
redirectWithQueryBuilderData,

View File

@@ -96,7 +96,7 @@ function BasicInfo({
return (
<>
<StepHeading> {t('alert_form_step3')} </StepHeading>
<StepHeading> {t('alert_form_step4')} </StepHeading>
<FormContainer>
<Form.Item
label={t('field_severity')}
@@ -189,6 +189,7 @@ function BasicInfo({
checked={shouldBroadCastToAllChannels}
onChange={handleBroadcastToAllChannels}
disabled={noChannels || !!channels.loading}
data-testid="alert-broadcast-to-all-channels"
/>
</Tooltip>
</FormItemMedium>

View File

@@ -63,6 +63,7 @@ function ChannelSelect({
mode="multiple"
style={{ width: '100%' }}
placeholder={t('placeholder_channel_select')}
data-testid="alert-channel-select"
value={currentValue}
onChange={(value): void => {
handleChange(value as string[]);

View File

@@ -0,0 +1,26 @@
.alert-chart-container {
height: 57vh;
width: 100%;
.threshold-alert-uplot-chart-container {
height: calc(100% - 24px);
}
.ant-card-body {
padding: 12px;
}
.anomaly-alert-evaluation-view-loading-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.anomaly-alert-evaluation-view-error-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@@ -1,8 +1,12 @@
import './ChartPreview.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
@@ -14,6 +18,7 @@ import {
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
@@ -34,6 +39,7 @@ import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { AlertDetectionTypes } from '..';
import { ChartContainer, FailedMessageContainer } from './styles';
import { getThresholdLabel } from './utils';
@@ -141,6 +147,7 @@ function ChartPreview({
selectedInterval,
minTime,
maxTime,
alertDef?.ruleType,
],
retry: false,
enabled: canQuery,
@@ -163,8 +170,6 @@ function ChartPreview({
queryResponse.data.payload.data.result = sortedSeriesData;
}
const chartData = getUPlotChartData(queryResponse?.data?.payload);
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
@@ -202,7 +207,10 @@ function ChartPreview({
id: 'alert_legend_widget',
yAxisUnit,
apiResponse: queryResponse?.data?.payload,
dimensions: containerDimensions,
dimensions: {
height: containerDimensions?.height ? containerDimensions.height - 48 : 0,
width: containerDimensions?.width,
},
minTimeScale,
maxTimeScale,
isDarkMode,
@@ -245,36 +253,59 @@ function ChartPreview({
],
);
const chartData = getUPlotChartData(queryResponse?.data?.payload);
const isAnomalyDetectionAlert =
alertDef?.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
const chartDataAvailable =
chartData && !queryResponse.isError && !queryResponse.isLoading;
const isAnomalyDetectionEnabled =
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
return (
<ChartContainer>
{headline}
<div className="alert-chart-container" ref={graphRef}>
<ChartContainer>
{headline}
<div ref={graphRef} style={{ height: '100%' }}>
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="100%" />
)}
{(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '}
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
<div className="threshold-alert-uplot-chart-container">
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="100%" />
)}
{(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{chartData && !queryResponse.isError && !queryResponse.isLoading && (
<GridPanelSwitch
options={options}
panelType={graphType}
data={chartData}
name={name || 'Chart Preview'}
panelData={
queryResponse.data?.payload?.data?.newResult?.data?.result || []
}
query={query || initialQueriesMap.metrics}
yAxisUnit={yAxisUnit}
/>
)}
</div>
</ChartContainer>
{chartDataAvailable && !isAnomalyDetectionAlert && (
<GridPanelSwitch
options={options}
panelType={graphType}
data={chartData}
name={name || 'Chart Preview'}
panelData={
queryResponse.data?.payload?.data?.newResult?.data?.result || []
}
query={query || initialQueriesMap.metrics}
yAxisUnit={yAxisUnit}
/>
)}
{chartDataAvailable &&
isAnomalyDetectionAlert &&
isAnomalyDetectionEnabled &&
queryResponse?.data?.payload?.data?.resultType === 'anomaly' && (
<AnomalyAlertEvaluationView
data={queryResponse?.data?.payload}
yAxisUnit={yAxisUnit}
/>
)}
</div>
</ChartContainer>
</div>
);
}

View File

@@ -21,6 +21,70 @@
}
}
.steps-container {
width: 80%;
}
.qb-chart-preview-container {
margin-bottom: 1rem;
display: flex;
flex-direction: row;
gap: 1rem;
}
.overview-header {
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
.alert-type-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
.alert-type-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.ant-typography {
margin: 0;
}
}
}
.chart-preview-container {
position: relative;
display: flex;
flex-direction: row;
gap: 1rem;
.ant-card {
flex: 1;
}
}
.detection-method-container {
margin: 24px 0;
.ant-tabs-nav {
margin-bottom: 0;
.ant-tabs-tab {
padding: 12px 0;
}
}
.detection-method-description {
padding: 8px 0;
font-size: 12px;
}
}
.info-help-btns {
display: grid;
grid-template-columns: auto auto;

View File

@@ -99,7 +99,7 @@ function QuerySection({
{
label: (
<Tooltip title="Query Builder">
<Button className="nav-btns">
<Button className="nav-btns" data-testid="query-builder-tab">
<Atom size={14} />
</Button>
</Tooltip>
@@ -222,7 +222,7 @@ function QuerySection({
};
return (
<>
<StepHeading> {t('alert_form_step1')}</StepHeading>
<StepHeading> {t('alert_form_step2')}</StepHeading>
<FormContainer>
<div>{renderTabs(alertType)}</div>
{renderQuerySection(currentTab)}

View File

@@ -0,0 +1,6 @@
.rule-definition {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}

View File

@@ -1,3 +1,5 @@
import './RuleOptions.styles.scss';
import {
Checkbox,
Collapse,
@@ -18,14 +20,17 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useTranslation } from 'react-i18next';
import {
AlertDef,
defaultAlgorithm,
defaultCompareOp,
defaultEvalWindow,
defaultFrequency,
defaultMatchType,
defaultSeasonality,
} from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
import { popupContainer } from 'utils/selectPopupContainer';
import { AlertDetectionTypes } from '.';
import {
FormContainer,
InlineSelect,
@@ -43,6 +48,8 @@ function RuleOptions({
const { t } = useTranslation('alerts');
const { currentQuery } = useQueryBuilder();
const { ruleType } = alertDef;
const handleMatchOptChange = (value: string | unknown): void => {
const m = (value as string) || alertDef.condition?.matchType;
setAlertDef({
@@ -86,8 +93,19 @@ function RuleOptions({
>
<Select.Option value="1">{t('option_above')}</Select.Option>
<Select.Option value="2">{t('option_below')}</Select.Option>
<Select.Option value="3">{t('option_equal')}</Select.Option>
<Select.Option value="4">{t('option_notequal')}</Select.Option>
{/* hide equal and not eqaul in case of analmoy based alert */}
{ruleType !== 'anomaly_rule' && (
<>
<Select.Option value="3">{t('option_equal')}</Select.Option>
<Select.Option value="4">{t('option_notequal')}</Select.Option>
</>
)}
{ruleType === 'anomaly_rule' && (
<Select.Option value="5">{t('option_above_below')}</Select.Option>
)}
</InlineSelect>
);
@@ -101,9 +119,14 @@ function RuleOptions({
>
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
<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>
{ruleType !== 'anomaly_rule' && (
<>
<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>
);
@@ -115,6 +138,28 @@ function RuleOptions({
});
};
const onChangeAlgorithm = (value: string | unknown): void => {
const alg = (value as string) || alertDef.condition.algorithm;
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
algorithm: alg,
},
});
};
const onChangeSeasonality = (value: string | unknown): void => {
const seasonality = (value as string) || alertDef.condition.seasonality;
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
seasonality,
},
});
};
const renderEvalWindows = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
@@ -146,6 +191,32 @@ function RuleOptions({
</InlineSelect>
);
const renderAlgorithms = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={defaultAlgorithm}
style={{ minWidth: '120px' }}
value={alertDef.condition.algorithm}
onChange={onChangeAlgorithm}
>
<Select.Option value="standard">Standard</Select.Option>
</InlineSelect>
);
const renderSeasonality = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={defaultSeasonality}
style={{ minWidth: '120px' }}
value={alertDef.condition.seasonality}
onChange={onChangeSeasonality}
>
<Select.Option value="hourly">Hourly</Select.Option>
<Select.Option value="daily">Daily</Select.Option>
<Select.Option value="weekly">Weekly</Select.Option>
</InlineSelect>
);
const renderThresholdRuleOpts = (): JSX.Element => (
<Form.Item>
<Typography.Text>
@@ -166,6 +237,39 @@ function RuleOptions({
</Form.Item>
);
const renderAnomalyRuleOpts = (
onChange: InputNumberProps['onChange'],
): JSX.Element => (
<Form.Item>
<Typography.Text className="rule-definition">
{t('text_condition1_anomaly')}
<InlineSelect
getPopupContainer={popupContainer}
allowClear
showSearch
options={queryOptions}
placeholder={t('selected_query_placeholder')}
value={alertDef.condition.selectedQueryName}
onChange={onChangeSelectedQueryName}
/>
{t('text_condition3')} {renderEvalWindows()}
<Typography.Text>is</Typography.Text>
<InputNumber
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
<Typography.Text>deviations</Typography.Text>
{renderCompareOps()}
<Typography.Text>the predicted data</Typography.Text>
{renderMatchOpts()}
using the {renderAlgorithms()} algorithm with {renderSeasonality()}{' '}
seasonality
</Typography.Text>
</Form.Item>
);
const renderPromRuleOptions = (): JSX.Element => (
<Form.Item>
<Typography.Text>
@@ -245,36 +349,46 @@ function RuleOptions({
return (
<>
<StepHeading>{t('alert_form_step2')}</StepHeading>
<StepHeading>{t('alert_form_step3')}</StepHeading>
<FormContainer>
{queryCategory === EQueryType.PROM
? renderPromRuleOptions()
: renderThresholdRuleOpts()}
{queryCategory === EQueryType.PROM && renderPromRuleOptions()}
{queryCategory !== EQueryType.PROM &&
ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
<>{renderAnomalyRuleOpts(onChange)}</>
)}
{queryCategory !== EQueryType.PROM &&
ruleType === AlertDetectionTypes.THRESHOLD_ALERT &&
renderThresholdRuleOpts()}
<Space direction="vertical" size="large">
<Space direction="horizontal" align="center">
<Form.Item noStyle name={['condition', 'target']}>
<InputNumber
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
{queryCategory !== EQueryType.PROM &&
ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
<Space direction="horizontal" align="center">
<Form.Item noStyle name={['condition', 'target']}>
<InputNumber
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
<Form.Item noStyle>
<Select
getPopupContainer={popupContainer}
allowClear
showSearch
options={categorySelectOptions}
placeholder={t('field_unit')}
value={alertDef.condition.targetUnit}
onChange={onChangeAlertUnit}
/>
</Form.Item>
</Space>
)}
<Form.Item noStyle>
<Select
getPopupContainer={popupContainer}
allowClear
showSearch
options={categorySelectOptions}
placeholder={t('field_unit')}
value={alertDef.condition.targetUnit}
onChange={onChangeAlertUnit}
/>
</Form.Item>
</Space>
<Collapse>
<Collapse.Panel header={t('More options')} key="1">
<Space direction="vertical" size="large">
@@ -323,6 +437,45 @@ function RuleOptions({
<Typography.Text>{t('text_for')}</Typography.Text>
</Space>
</VerticalLine>
<VerticalLine>
<Space direction="horizontal" align="center">
<Form.Item noStyle name={['condition', 'requireMinPoints']}>
<Checkbox
checked={alertDef?.condition?.requireMinPoints}
onChange={(e): void => {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
requireMinPoints: e.target.checked,
},
});
}}
/>
</Form.Item>
<Typography.Text>{t('text_require_min_points')}</Typography.Text>
<Form.Item noStyle name={['condition', 'requiredNumPoints']}>
<InputNumber
min={1}
value={alertDef?.condition?.requiredNumPoints}
onChange={(value): void => {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
requiredNumPoints: Number(value) || 0,
},
});
}}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
<Typography.Text>{t('text_num_points')}</Typography.Text>
</Space>
</VerticalLine>
</Space>
</Collapse.Panel>
</Collapse>

View File

@@ -3,7 +3,6 @@ import './FormAlertRules.styles.scss';
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import {
Button,
Col,
FormInstance,
Modal,
SelectProps,
@@ -13,8 +12,6 @@ import {
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { alertHelpMessage } from 'components/LaunchChatSupport/util';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
@@ -26,13 +23,18 @@ import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import useFeatureFlag, {
MESSAGE,
useIsFeatureDisabled,
} from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEqual } from 'lodash-es';
import { BellDot, ExternalLink } from 'lucide-react';
import Tabs2 from 'periscope/components/Tabs2';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
@@ -44,7 +46,11 @@ import {
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
Query,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -56,13 +62,16 @@ import {
ActionButton,
ButtonContainer,
MainFormContainer,
PanelContainer,
StepContainer,
StyledLeftContainer,
StepHeading,
} from './styles';
import UserGuide from './UserGuide';
import { getSelectedQueryOptions } from './utils';
export enum AlertDetectionTypes {
THRESHOLD_ALERT = 'threshold_rule',
ANOMALY_DETECTION_ALERT = 'anomaly_rule',
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function FormAlertRules({
alertType,
@@ -86,6 +95,7 @@ function FormAlertRules({
const {
currentQuery,
stagedQuery,
handleSetQueryData,
handleRunQuery,
handleSetConfig,
initialDataSource,
@@ -108,6 +118,12 @@ function FormAlertRules({
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
const [detectionMethod, setDetectionMethod] = useState<string>(
AlertDetectionTypes.THRESHOLD_ALERT,
);
useEffect(() => {
if (!isEqual(currentQuery.unit, yAxisUnit)) {
setYAxisUnit(currentQuery.unit || '');
@@ -138,6 +154,45 @@ function FormAlertRules({
useShareBuilderUrl(sq);
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
const anomalyFunction = {
name: 'anomaly',
args: [],
namedArgs: { z_score_threshold: 9 },
};
const functions = data.functions || [];
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
// Add anomaly if not already present
if (!functions.some((func) => func.name === 'anomaly')) {
functions.push(anomalyFunction);
}
} else {
// Remove anomaly if present
const index = functions.findIndex((func) => func.name === 'anomaly');
if (index !== -1) {
functions.splice(index, 1);
}
}
return functions;
};
const updateFunctionsBasedOnAlertType = (): void => {
for (let index = 0; index < currentQuery.builder.queryData.length; index++) {
const queryData = currentQuery.builder.queryData[index];
const updatedFunctions = updateFunctions(queryData);
queryData.functions = updatedFunctions;
handleSetQueryData(index, queryData);
}
};
useEffect(() => {
updateFunctionsBasedOnAlertType();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [detectionMethod, alertDef, currentQuery.builder.queryData.length]);
useEffect(() => {
const broadcastToSpecificChannels =
(initialValue &&
@@ -145,11 +200,22 @@ function FormAlertRules({
initialValue.preferredChannels.length > 0) ||
isNewRule;
let ruleType = AlertDetectionTypes.THRESHOLD_ALERT;
if (initialValue.ruleType) {
ruleType = initialValue.ruleType as AlertDetectionTypes;
} else if (alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
ruleType = AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
}
setAlertDef({
...initialValue,
broadcastToAll: !broadcastToSpecificChannels,
ruleType,
});
}, [initialValue, isNewRule]);
setDetectionMethod(ruleType);
}, [initialValue, isNewRule, alertTypeFromURL]);
useEffect(() => {
// Set selectedQueryName based on the length of queryOptions
@@ -300,12 +366,15 @@ function FormAlertRules({
const postableAlert: AlertDef = {
...alertDef,
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
alertType,
alertType:
alertType === AlertTypes.ANOMALY_BASED_ALERT
? AlertTypes.METRICS_BASED_ALERT
: alertType,
source: window?.location.toString(),
ruleType:
currentQuery.queryType === EQueryType.PROM
? 'promql_rule'
: 'threshold_rule',
: alertDef.ruleType,
condition: {
...alertDef.condition,
compositeQuery: {
@@ -322,6 +391,12 @@ function FormAlertRules({
},
},
};
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
postableAlert.condition.algorithm = alertDef.condition.algorithm;
postableAlert.condition.seasonality = alertDef.condition.seasonality;
}
return postableAlert;
};
@@ -585,63 +660,102 @@ function FormAlertRules({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function handleRedirection(option: AlertTypes): void {
let url = '';
switch (option) {
case AlertTypes.METRICS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.LOGS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.TRACES_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.EXCEPTIONS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
default:
break;
}
logEvent('Alert: Check example alert clicked', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
isNewRule: !ruleId || ruleId === 0,
ruleId,
queryType: currentQuery.queryType,
link: url,
});
window.open(url, '_blank');
}
const tabs = [
{
value: AlertDetectionTypes.THRESHOLD_ALERT,
label: 'Threshold Alert',
},
{
value: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
label: 'Anomaly Detection Alert',
isBeta: true,
},
];
const handleDetectionMethodChange = (value: any): void => {
setAlertDef((def) => ({
...def,
ruleType: value,
}));
setDetectionMethod(value);
};
const isAnomalyDetectionEnabled =
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
return (
<>
{Element}
<PanelContainer id="top">
<StyledLeftContainer flex="5 1 600px" md={18}>
<MainFormContainer
initialValues={initialValue}
layout="vertical"
form={formInstance}
className="main-container"
>
<div id="top">
<div className="overview-header">
<div className="alert-type-container">
{isNewRule && (
<Typography.Title level={5} className="alert-type-title">
<BellDot size={14} />
{alertDef.alertType === AlertTypes.ANOMALY_BASED_ALERT &&
'Anomaly Detection Alert'}
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
'Metrics Based Alert'}
{alertDef.alertType === AlertTypes.LOGS_BASED_ALERT &&
'Logs Based Alert'}
{alertDef.alertType === AlertTypes.TRACES_BASED_ALERT &&
'Traces Based Alert'}
{alertDef.alertType === AlertTypes.EXCEPTIONS_BASED_ALERT &&
'Exceptions Based Alert'}
</Typography.Title>
)}
</div>
<Button className="periscope-btn" icon={<ExternalLink size={14} />}>
Alert Setup Guide
</Button>
</div>
<MainFormContainer
initialValues={initialValue}
layout="vertical"
form={formInstance}
className="main-container"
>
<div className="chart-preview-container">
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
renderQBChartPreview()}
{currentQuery.queryType === EQueryType.PROM &&
renderPromAndChQueryChartPreview()}
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
renderPromAndChQueryChartPreview()}
</div>
<StepContainer>
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
</StepContainer>
<StepContainer>
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
</StepContainer>
<div className="steps-container">
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
isAnomalyDetectionEnabled && (
<div className="detection-method-container">
<StepHeading> {t('alert_form_step1')}</StepHeading>
<Tabs2
key={detectionMethod}
tabs={tabs}
initialSelectedTab={detectionMethod}
onSelectTab={handleDetectionMethodChange}
/>
<div className="detection-method-description">
{detectionMethod === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
? t('anomaly_detection_alert_desc')
: t('threshold_alert_desc')}
</div>
</div>
)}
<QuerySection
queryCategory={currentQuery.queryType}
@@ -662,79 +776,49 @@ function FormAlertRules({
/>
{renderBasicInfo()}
<ButtonContainer>
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
<ActionButton
loading={loading || false}
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
disabled={
isAlertNameMissing ||
isAlertAvailableToSave ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
>
{isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton>
</Tooltip>
</div>
<ButtonContainer>
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
<ActionButton
loading={loading || false}
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
disabled={
isAlertNameMissing ||
isAlertAvailableToSave ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
type="default"
onClick={onTestRuleHandler}
>
{' '}
{t('button_testrule')}
{isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton>
<ActionButton
disabled={loading || false}
type="default"
onClick={onCancelHandler}
>
{ruleId === 0 && t('button_cancelchanges')}
{ruleId > 0 && t('button_discard')}
</ActionButton>
</ButtonContainer>
</MainFormContainer>
</StyledLeftContainer>
<Col flex="1 1 300px">
<UserGuide queryType={currentQuery.queryType} />
<div className="info-help-btns">
<Button
style={{ height: 32 }}
onClick={(): void =>
handleRedirection(alertDef?.alertType as AlertTypes)
</Tooltip>
<ActionButton
loading={loading || false}
disabled={
isAlertNameMissing ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
className="doc-redirection-btn"
type="default"
onClick={onTestRuleHandler}
>
Check an example alert
</Button>
<LaunchChatSupport
attributes={{
alert: alertDef?.alert,
alertType: alertDef?.alertType,
id: ruleId,
ruleType: alertDef?.ruleType,
state: (alertDef as any)?.state,
panelType,
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
}}
className="facing-issue-btn"
eventName="Alert: Facing Issues in alert"
buttonText="Need help with this alert?"
message={alertHelpMessage(alertDef, ruleId)}
onHoverText="Click here to get help with this alert"
/>
</div>
</Col>
</PanelContainer>
{' '}
{t('button_testrule')}
</ActionButton>
<ActionButton
disabled={loading || false}
type="default"
onClick={onCancelHandler}
>
{ruleId === 0 && t('button_cancelchanges')}
{ruleId > 0 && t('button_discard')}
</ActionButton>
</ButtonContainer>
</MainFormContainer>
</div>
</>
);
}

View File

@@ -1,13 +1,9 @@
import { Button, Card, Col, Form, Input, Row, Select, Typography } from 'antd';
import { Button, Card, Col, Form, Input, Select, Typography } from 'antd';
import styled from 'styled-components';
const { TextArea } = Input;
const { Item } = Form;
export const PanelContainer = styled(Row)`
flex-wrap: nowrap;
`;
export const StyledLeftContainer = styled(Col)`
&&& {
margin-right: 1rem;

View File

@@ -20,6 +20,14 @@
}
}
.widget-header-title-container {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
text-overflow: ellipsis;
}
.widget-header-title {
max-width: 80%;
}
@@ -58,3 +66,17 @@
}
}
}
.long-tooltip {
.ant-tooltip-content {
max-height: 500px;
overflow: auto;
}
&.ant-tooltip {
max-width: 500px;
}
}
.info-tooltip {
cursor: pointer;
}

View File

@@ -6,8 +6,8 @@ import {
CopyOutlined,
DeleteOutlined,
EditFilled,
ExclamationCircleOutlined,
FullscreenOutlined,
InfoCircleOutlined,
MoreOutlined,
SearchOutlined,
WarningOutlined,
@@ -21,7 +21,7 @@ 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 { CircleX, X } from 'lucide-react';
import { unparse } from 'papaparse';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
@@ -234,13 +234,25 @@ function WidgetHeader({
/>
) : (
<>
<Typography.Text
ellipsis
data-testid={title}
className="widget-header-title"
>
{title}
</Typography.Text>
<div className="widget-header-title-container">
<Typography.Text
ellipsis
data-testid={title}
className="widget-header-title"
>
{title}
</Typography.Text>
{widget.description && (
<Tooltip
title={widget.description}
overlayClassName="long-tooltip"
className="info-tooltip"
placement="right"
>
<InfoCircleOutlined />
</Tooltip>
)}
</div>
<div className="widget-header-actions">
<div className="widget-api-actions">{threshold}</div>
{isFetchingResponse && !queryResponse.isError && (
@@ -252,7 +264,7 @@ function WidgetHeader({
placement={errorTooltipPosition}
className="widget-api-actions"
>
<ExclamationCircleOutlined />
<CircleX size={20} />
</Tooltip>
)}

View File

@@ -14,10 +14,12 @@ import {
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import history from 'lib/history';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
import store from 'store';
import { UpdateTimeInterval } from 'store/actions';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -123,6 +125,16 @@ function DBCall(): JSX.Element {
[servicename, tagFilterItems],
);
const stepInterval = useMemo(
() =>
getStep({
end: store.getState().globalTime.maxTime,
inputFormat: 'ns',
start: store.getState().globalTime.minTime,
}),
[],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
@@ -158,6 +170,7 @@ function DBCall(): JSX.Element {
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
})}
>
View Traces
@@ -192,6 +205,7 @@ function DBCall(): JSX.Element {
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
})}
>
View Traces

View File

@@ -16,10 +16,12 @@ import {
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import history from 'lib/history';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
import store from 'store';
import { UpdateTimeInterval } from 'store/actions';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
@@ -141,6 +143,15 @@ function External(): JSX.Element {
],
});
const stepInterval = useMemo(
() =>
getStep({
end: store.getState().globalTime.maxTime,
inputFormat: 'ns',
start: store.getState().globalTime.minTime,
}),
[],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
@@ -222,6 +233,7 @@ function External(): JSX.Element {
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: errorApmToTraceQuery,
stepInterval,
})}
>
View Traces
@@ -257,6 +269,7 @@ function External(): JSX.Element {
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
})}
>
View Traces
@@ -295,6 +308,7 @@ function External(): JSX.Element {
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
})}
>
View Traces
@@ -330,6 +344,7 @@ function External(): JSX.Element {
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
})}
>
View Traces

View File

@@ -15,6 +15,7 @@ import {
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import history from 'lib/history';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { defaultTo } from 'lodash-es';
@@ -38,6 +39,7 @@ import {
} from '../MetricsPageQueries/OverviewQueries';
import { Col, ColApDexContainer, ColErrorContainer, Row } from '../styles';
import ApDex from './Overview/ApDex';
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
import ServiceOverview from './Overview/ServiceOverview';
import TopLevelOperation from './Overview/TopLevelOperations';
import TopOperation from './Overview/TopOperation';
@@ -45,9 +47,11 @@ import TopOperationMetrics from './Overview/TopOperationMetrics';
import { Button, Card } from './styles';
import { IServiceName } from './types';
import {
generateExplorerPath,
handleNonInQueryRange,
onGraphClickHandler,
onViewTracePopupClick,
useGetAPMToLogsQueries,
useGetAPMToTracesQueries,
} from './util';
@@ -177,6 +181,16 @@ function Application(): JSX.Element {
id: SERVICE_CHART_ID.errorPercentage,
});
const stepInterval = useMemo(
() =>
getStep({
end: maxTime,
inputFormat: 'ns',
start: minTime,
}),
[maxTime, minTime],
);
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
@@ -194,33 +208,60 @@ function Application(): JSX.Element {
[dispatch, pathname, urlQuery],
);
const onErrorTrackHandler = (
timestamp: number,
apmToTraceQuery: Query,
): (() => void) => (): void => {
const currentTime = timestamp;
const tPlusOne = timestamp + 60 * 1000;
const onErrorTrackHandler = useCallback(
(
timestamp: number,
apmToTraceQuery: Query,
isViewLogsClicked?: boolean,
): (() => void) => (): void => {
const currentTime = timestamp;
const endTime = timestamp + stepInterval;
console.log(endTime, stepInterval);
const urlParams = new URLSearchParams(search);
urlParams.set(QueryParams.startTime, currentTime.toString());
urlParams.set(QueryParams.endTime, tPlusOne.toString());
const urlParams = new URLSearchParams(search);
urlParams.set(QueryParams.startTime, currentTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams);
const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams);
const JSONCompositeQuery = encodeURIComponent(
JSON.stringify(apmToTraceQuery),
);
const JSONCompositeQuery = encodeURIComponent(
JSON.stringify(apmToTraceQuery),
);
const newPath = generateExplorerPath(
isViewLogsClicked,
urlParams,
servicename,
selectedTraceTags,
JSONCompositeQuery,
queryString,
);
const newTraceExplorerPath = `${
ROUTES.TRACES_EXPLORER
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&${
QueryParams.compositeQuery
}=${JSONCompositeQuery}&${queryString.join('&')}`;
history.push(newTraceExplorerPath);
};
history.push(newPath);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[stepInterval],
);
const logErrorQuery = useGetAPMToLogsQueries({
servicename,
filters: [
{
id: uuid().slice(0, 8),
key: {
key: 'severity_text',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'severity_text--string----true',
},
op: 'in',
value: ['ERROR', 'FATAL', 'error', 'fatal'],
},
],
});
const errorTrackQuery = useGetAPMToTracesQueries({
servicename,
filters: [
@@ -251,6 +292,7 @@ function Application(): JSX.Element {
selectedTraceTags={selectedTraceTags}
topLevelOperationsRoute={topLevelOperationsRoute}
topLevelOperationsIsLoading={topLevelOperationsIsLoading}
stepInterval={stepInterval}
/>
</Col>
@@ -264,6 +306,7 @@ function Application(): JSX.Element {
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
})}
>
View Traces
@@ -292,6 +335,7 @@ function Application(): JSX.Element {
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
})}
>
View Traces
@@ -304,14 +348,18 @@ function Application(): JSX.Element {
/>
</ColApDexContainer>
<ColErrorContainer>
<Button
type="default"
size="small"
<GraphControlsPanel
id="Error_button"
onClick={onErrorTrackHandler(selectedTimeStamp, errorTrackQuery)}
>
View Traces
</Button>
onViewLogsClick={onErrorTrackHandler(
selectedTimeStamp,
logErrorQuery,
true,
)}
onViewTracesClick={onErrorTrackHandler(
selectedTimeStamp,
errorTrackQuery,
)}
/>
<TopLevelOperation
handleGraphClick={handleGraphClick}

View File

@@ -0,0 +1,25 @@
.graph-controls-panel {
position: absolute;
z-index: 999;
display: none;
width: 110px;
padding: 5px;
border-radius: 5px;
background: var(--bg-slate-400);
.ant-btn-link {
padding: 0;
margin: 0;
text-decoration: none;
color: var(--bg-vanilla-100);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 2px;
border: none;
}
.ant-btn-link:hover {
color: var(--bg-vanilla-100);
}
}

View File

@@ -0,0 +1,42 @@
import './GraphControlsPanel.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { DraftingCompass, ScrollText } from 'lucide-react';
interface GraphControlsPanelProps {
id: string;
onViewLogsClick: () => void;
onViewTracesClick: () => void;
}
function GraphControlsPanel({
id,
onViewLogsClick,
onViewTracesClick,
}: GraphControlsPanelProps): JSX.Element {
return (
<div id={id} className="graph-controls-panel">
<Button
type="link"
icon={<DraftingCompass size={14} />}
size="small"
onClick={onViewTracesClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View traces
</Button>
<Button
type="link"
icon={<ScrollText size={14} />}
size="small"
onClick={onViewLogsClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View logs
</Button>
</div>
);
}
export default GraphControlsPanel;

View File

@@ -19,13 +19,14 @@ import { useParams } from 'react-router-dom';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { Button } from '../styles';
import { IServiceName } from '../types';
import {
handleNonInQueryRange,
onViewTracePopupClick,
useGetAPMToLogsQueries,
useGetAPMToTracesQueries,
} from '../util';
import GraphControlsPanel from './GraphControlsPanel/GraphControlsPanel';
function ServiceOverview({
onDragSelect,
@@ -34,6 +35,7 @@ function ServiceOverview({
selectedTimeStamp,
topLevelOperationsRoute,
topLevelOperationsIsLoading,
stepInterval,
}: ServiceOverviewProps): JSX.Element {
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
@@ -75,21 +77,28 @@ function ServiceOverview({
const apmToTraceQuery = useGetAPMToTracesQueries({ servicename });
const apmToLogQuery = useGetAPMToLogsQueries({ servicename });
return (
<>
<Button
type="default"
size="small"
<GraphControlsPanel
id="Service_button"
onClick={onViewTracePopupClick({
onViewLogsClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: apmToLogQuery,
isViewLogsClicked: true,
stepInterval,
})}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
})}
>
View Traces
</Button>
/>
<Card data-testid="service_latency">
<GraphContainer>
{topLevelOperationsIsLoading && (
@@ -114,8 +123,8 @@ function ServiceOverview({
</>
);
}
interface ServiceOverviewProps {
stepInterval: number;
selectedTimeStamp: number;
selectedTraceTags: string;
onDragSelect: (start: number, end: number) => void;

View File

@@ -7,6 +7,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils';
import history from 'lib/history';
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
import { Dispatch, SetStateAction, useMemo } from 'react';
import {
@@ -33,21 +34,44 @@ interface OnViewTracePopupClickProps {
selectedTraceTags: string;
timestamp: number;
apmToTraceQuery: Query;
isViewLogsClicked?: boolean;
stepInterval?: number;
}
export function generateExplorerPath(
isViewLogsClicked: boolean | undefined,
urlParams: URLSearchParams,
servicename: string | undefined,
selectedTraceTags: string,
JSONCompositeQuery: string,
queryString: string[],
): string {
const basePath = isViewLogsClicked
? ROUTES.LOGS_EXPLORER
: ROUTES.TRACES_EXPLORER;
return `${basePath}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&${
QueryParams.compositeQuery
}=${JSONCompositeQuery}&${queryString.join('&')}`;
}
// TODO(@rahul-signoz): update the name of this function once we have view logs button in every panel
export function onViewTracePopupClick({
selectedTraceTags,
servicename,
timestamp,
apmToTraceQuery,
isViewLogsClicked,
stepInterval,
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
const currentTime = timestamp;
const tPlusOne = timestamp + 60;
const endTime = timestamp + (stepInterval || 60);
const urlParams = new URLSearchParams(window.location.search);
urlParams.set(QueryParams.startTime, currentTime.toString());
urlParams.set(QueryParams.endTime, tPlusOne.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams);
@@ -55,13 +79,16 @@ export function onViewTracePopupClick({
JSON.stringify(apmToTraceQuery),
);
const newTraceExplorerPath = `${
ROUTES.TRACES_EXPLORER
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&${
QueryParams.compositeQuery
}=${JSONCompositeQuery}&${queryString.join('&')}`;
const newPath = generateExplorerPath(
isViewLogsClicked,
urlParams,
servicename,
selectedTraceTags,
JSONCompositeQuery,
queryString,
);
history.push(newTraceExplorerPath);
history.push(newPath);
};
}
@@ -108,12 +135,13 @@ export function handleQueryChange(
attributeKeys: BaseAutocompleteData,
serviceAttribute: string,
filters?: TagFilterItem[],
logs?: boolean,
): Query {
const filterItem: TagFilterItem[] = [
{
id: uuid().slice(0, 8),
key: attributeKeys,
op: 'in',
op: logs ? '=' : 'in',
value: serviceAttribute,
},
];
@@ -132,6 +160,42 @@ export function handleQueryChange(
};
}
export function useGetAPMToLogsQueries({
servicename,
filters,
}: {
servicename: string;
filters?: TagFilterItem[];
}): Query {
const finalFilters: TagFilterItem[] = [];
const { updateAllQueriesOperators } = useQueryBuilder();
let updatedQuery = updateAllQueriesOperators(
initialQueriesMap.logs,
PANEL_TYPES.LIST,
DataSource.LOGS,
);
const serviceName = {
id: 'service.name--string--resource--true',
dataType: DataTypes.String,
isColumn: false,
key: 'service.name',
type: 'resource',
isJSON: false,
};
if (filters?.length) {
finalFilters.push(...filters);
}
updatedQuery = prepareQueryWithDefaultTimestamp(updatedQuery);
return handleQueryChange(
updatedQuery,
serviceName,
servicename,
finalFilters,
true,
);
}
export function useGetAPMToTracesQueries({
servicename,
isExternalCall,

View File

@@ -182,6 +182,15 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { t } = useTranslation(['dashboard', 'common']);
// used to set the initial value for the updatedTitle
// the context value is sometimes not available during the initial render
// due to which the updatedTitle is set to some previous value
useEffect(() => {
if (selectedDashboard) {
setUpdatedTitle(selectedDashboard.data.title);
}
}, [selectedDashboard]);
useEffect(() => {
if (state.error) {
notifications.error({

View File

@@ -257,8 +257,7 @@ function VariableItem({
if (variableData.name) {
if (
value === ALL_SELECT_VALUE ||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
(Array.isArray(value) && value.length === 0)
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
) {
onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else {
@@ -324,10 +323,6 @@ function VariableItem({
Array.isArray(selectedValueStringified) &&
selectedValueStringified.includes(option.toString())
) {
if (newSelectedValue.length === 0) {
handleChange(ALL_SELECT_VALUE);
return;
}
if (newSelectedValue.length === 1) {
handleChange(newSelectedValue[0].toString());
return;

View File

@@ -1,14 +1,17 @@
import './QuerySection.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Tooltip, Typography } from 'antd';
import { Button, Tabs, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import { getDefaultWidgetData } from 'container/NewWidget/utils';
import {
getDefaultWidgetData,
PANEL_TYPE_TO_QUERY_TYPES,
} from 'container/NewWidget/utils';
import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
@@ -112,16 +115,18 @@ function QuerySection({
],
);
const handleQueryCategoryChange = (qCategory: string): void => {
const currentQueryType = qCategory;
featureResponse.refetch().then(() => {
handleStageQuery({
...currentQuery,
queryType: currentQueryType as EQueryType,
const handleQueryCategoryChange = useCallback(
(qCategory: string): void => {
const currentQueryType = qCategory;
featureResponse.refetch().then(() => {
handleStageQuery({
...currentQuery,
queryType: currentQueryType as EQueryType,
});
});
});
};
},
[currentQuery, featureResponse, handleStageQuery],
);
const handleRunQuery = (): void => {
const widgetId = urlQuery.get('widgetId');
@@ -147,72 +152,55 @@ function QuerySection({
return config;
}, []);
const listItems = [
{
key: EQueryType.QUERY_BUILDER,
label: (
<Button className="nav-btns">
<Atom size={14} />
<Typography>Query Builder</Typography>
</Button>
),
tab: <Typography>Query Builder</Typography>,
children: (
<QueryBuilder
panelType={PANEL_TYPES.LIST}
filterConfigs={filterConfigs}
version={selectedDashboard?.data?.version || 'v3'}
isListViewPanel
/>
),
},
];
const items = useMemo(() => {
const supportedQueryTypes = PANEL_TYPE_TO_QUERY_TYPES[selectedGraph] || [];
const items = [
{
key: EQueryType.QUERY_BUILDER,
const queryTypeComponents = {
[EQueryType.QUERY_BUILDER]: {
icon: <Atom size={14} />,
label: 'Query Builder',
component: (
<QueryBuilder
panelType={selectedGraph}
filterConfigs={filterConfigs}
version={selectedDashboard?.data?.version || 'v3'}
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
/>
),
},
[EQueryType.CLICKHOUSE]: {
icon: <Terminal size={14} />,
label: 'ClickHouse Query',
component: <ClickHouseQueryContainer />,
},
[EQueryType.PROM]: {
icon: (
<PromQLIcon
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
/>
),
label: 'PromQL',
component: <PromQLQueryContainer />,
},
};
return supportedQueryTypes.map((queryType) => ({
key: queryType,
label: (
<Button className="nav-btns">
<Atom size={14} />
<Typography>Query Builder</Typography>
{queryTypeComponents[queryType].icon}
<Typography>{queryTypeComponents[queryType].label}</Typography>
</Button>
),
tab: <Typography>Query Builder</Typography>,
children: (
<QueryBuilder
panelType={selectedGraph}
filterConfigs={filterConfigs}
version={selectedDashboard?.data?.version || 'v3'}
/>
),
},
{
key: EQueryType.CLICKHOUSE,
label: (
<Button className="nav-btns">
<Terminal size={14} />
<Typography>ClickHouse Query</Typography>
</Button>
),
tab: <Typography>ClickHouse Query</Typography>,
children: <ClickHouseQueryContainer />,
},
{
key: EQueryType.PROM,
label: (
<Tooltip title="PromQL">
<Button className="nav-btns">
<PromQLIcon
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
/>
<Typography>PromQL</Typography>
</Button>
</Tooltip>
),
tab: <Typography>PromQL</Typography>,
children: <PromQLQueryContainer />,
},
];
tab: <Typography>{queryTypeComponents[queryType].label}</Typography>,
children: queryTypeComponents[queryType].component,
}));
}, [
selectedGraph,
filterConfigs,
selectedDashboard?.data?.version,
isDarkMode,
]);
useEffect(() => {
registerShortcut(QBShortcuts.StageAndRunQuery, handleRunQuery);
@@ -223,6 +211,16 @@ function QuerySection({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleRunQuery]);
useEffect(() => {
// switch to query builder if query type is not supported
if (
(selectedGraph === PANEL_TYPES.TABLE || selectedGraph === PANEL_TYPES.PIE) &&
currentQuery.queryType === EQueryType.PROM
) {
handleQueryCategoryChange(EQueryType.QUERY_BUILDER);
}
}, [currentQuery, handleQueryCategoryChange, selectedGraph]);
return (
<div className="dashboard-navigation">
<Tabs
@@ -267,7 +265,7 @@ function QuerySection({
</Button>
</span>
}
items={selectedGraph === PANEL_TYPES.LIST ? listItems : items}
items={items}
/>
</div>
);

View File

@@ -11,6 +11,7 @@ import {
import { cloneDeep, isEqual, set, unset } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
export const getIsQueryModified = (
@@ -492,3 +493,39 @@ export const getDefaultWidgetData = (
...listViewInitialTraceQuery.builder.queryData[0].selectColumns,
],
});
export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {
[PANEL_TYPES.TIME_SERIES]: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
[PANEL_TYPES.TABLE]: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
[PANEL_TYPES.VALUE]: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
[PANEL_TYPES.LIST]: [EQueryType.QUERY_BUILDER],
[PANEL_TYPES.TRACE]: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
[PANEL_TYPES.BAR]: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
[PANEL_TYPES.PIE]: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
[PANEL_TYPES.HISTOGRAM]: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
[PANEL_TYPES.EMPTY_WIDGET]: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
};

View File

@@ -60,7 +60,7 @@ This is a **sample cURL request** which can be used as a template:
&nbsp;
```bash
curl --location 'https://ingest.{{REGION}}.signoz.cloud:443/logs/json/' \
curl --location 'https://ingest.{{REGION}}.signoz.cloud:443/logs/json' \
--header 'Content-Type: application/json' \
--header 'signoz-access-token: {{SIGNOZ_INGESTION_KEY}}' \
--data '[

View File

@@ -333,7 +333,7 @@ export const Query = memo(function Query({
const isVersionV4 = version && version === ENTITY_VERSION_V4;
return (
<Row gutter={[0, 12]}>
<Row gutter={[0, 12]} className={`query-builder-${version}`}>
<QBEntityOptions
isMetricsDataSource={isMetricsDataSource}
showFunctions={

View File

@@ -33,7 +33,7 @@ export default function Function({
handleDeleteFunction,
}: FunctionProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { showInput } = queryFunctionsTypesConfig[funcData.name];
const { showInput, disabled } = queryFunctionsTypesConfig[funcData.name];
let functionValue;
@@ -62,6 +62,7 @@ export default function Function({
<Select
className={cx('query-function-name-selector', showInput ? 'showInput' : '')}
value={funcData.name}
disabled={disabled}
style={{ minWidth: '100px' }}
onChange={(value): void => {
handleUpdateFunctionName(funcData, index, value);

View File

@@ -59,9 +59,10 @@ export const useOrderByFilter = ({
];
}, [searchText]);
const selectedValue = useMemo(() => transformToOrderByStringValues(query), [
query,
]);
const selectedValue = useMemo(
() => transformToOrderByStringValues(query, entityVersion),
[query, entityVersion],
);
const generateOptions = useCallback(
(options: IOption[]): IOption[] => {

View File

@@ -13,11 +13,14 @@ export const orderByValueDelimiter = '|';
export const transformToOrderByStringValues = (
query: IBuilderQuery,
entityVersion?: string,
): IOption[] => {
const prepareSelectedValue: IOption[] = query.orderBy.map((item) => {
if (item.columnName === SIGNOZ_VALUE) {
return {
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${item.order}`,
label: `${
entityVersion === 'v4' ? query.spaceAggregation : query.aggregateOperator
}(${query.aggregateAttribute.key}) ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
}

View File

@@ -334,8 +334,8 @@
.qb-search-bar-tokenised-tags {
.ant-tag {
border: 1px solid var(--bg-slate-100);
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-100);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
.ant-typography {

View File

@@ -244,7 +244,7 @@ function QueryBuilderSearchV2(
isFetching: isFetchingSuggestions,
} = useGetAttributeSuggestions(
{
searchText: searchValue.split(' ')[0],
searchText: searchValue?.split(' ')[0],
dataSource: query.dataSource,
filters: query.filters,
},
@@ -691,7 +691,7 @@ function QueryBuilderSearchV2(
}
}
if (currentState === DropdownState.OPERATOR) {
const keyOperator = searchValue.split(' ');
const keyOperator = searchValue?.split(' ');
const partialOperator = keyOperator?.[1];
const strippedKey = keyOperator?.[0];

View File

@@ -13,6 +13,7 @@
width: 5px;
border-radius: 50%;
background-color: var(--bg-slate-300);
flex-shrink: 0;
}
.option {
@@ -207,6 +208,10 @@
background: var(--bg-vanilla-300);
}
}
.value {
color: var(--bg-ink-100);
}
}
}
.option:hover {

View File

@@ -1,3 +1,4 @@
import { AttributeValuesMap } from 'components/ClientSideQBSearch/ClientSideQBSearch';
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
import { IOption } from 'hooks/useResourceAttribute/types';
import uniqWith from 'lodash-es/unionWith';
@@ -92,3 +93,20 @@ export const getValidOrderByResult = (result: IOption[]): IOption[] =>
return acc;
}, []);
export const transformKeyValuesToAttributeValuesMap = (
attributeValuesMap: Record<string, string[] | number[] | boolean[]>,
): AttributeValuesMap =>
Object.fromEntries(
Object.entries(attributeValuesMap || {}).map(([key, values]) => [
key,
{
stringAttributeValues:
typeof values[0] === 'string' ? (values as string[]) : [],
numberAttributeValues:
typeof values[0] === 'number' ? (values as number[]) : [],
boolAttributeValues:
typeof values[0] === 'boolean' ? (values as boolean[]) : [],
},
]),
);

View File

@@ -1,4 +1,5 @@
import { initialAutocompleteData, OPERATORS } from 'constants/queryBuilder';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import getStep from 'lib/getStep';
import {
BaseAutocompleteData,
@@ -27,7 +28,8 @@ export const getTraceToLogsQuery = (
items: [
{
id: uuid(),
op: OPERATORS.IN,
// for generating query we use in instead of IN
op: getOperatorValue(OPERATORS.IN),
value: traceId,
key,
},

View File

@@ -1,4 +1,4 @@
import { Button, Modal, Tabs, Tooltip, Typography } from 'antd';
import { Button, Modal, Row, Tabs, Tooltip, Typography } from 'antd';
import Editor from 'components/Editor';
import { StyledSpace } from 'components/Styled';
import { QueryParams } from 'constants/query';
@@ -6,7 +6,8 @@ import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { useState } from 'react';
import { PanelRight } from 'lucide-react';
import { Dispatch, SetStateAction, useState } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
@@ -28,6 +29,7 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
firstSpanStartTime,
traceStartTime = minTime,
traceEndTime = maxTime,
setCollapsed,
} = props;
const { id: traceId } = useParams<Params>();
@@ -96,14 +98,14 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
styledclass={[styles.selectedSpanDetailsContainer, styles.overflow]}
direction="vertical"
>
<Typography.Text
strong
style={{
marginTop: '16px',
}}
>
Details for selected Span
</Typography.Text>
<Row align="middle" justify="space-between">
<Typography.Text strong>Details for selected Span</Typography.Text>
<Button
className="periscope-btn nav-item-label expand-collapse-btn"
icon={<PanelRight size={16} />}
onClick={(): void => setCollapsed((prev) => !prev)}
/>
</Row>
<Typography.Text style={{ fontWeight: 700 }}>Service</Typography.Text>
@@ -170,6 +172,7 @@ interface SelectedSpanDetailsProps {
firstSpanStartTime: number;
traceStartTime?: number;
traceEndTime?: number;
setCollapsed: Dispatch<SetStateAction<boolean>>;
}
SelectedSpanDetails.defaultProps = {

View File

@@ -20,4 +20,10 @@
background-color: white !important;
}
}
.expand-collapse-btn {
padding: 4px;
height: auto;
width: auto;
}
}

View File

@@ -22,6 +22,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { spanServiceNameToColorMapping } from 'lib/getRandomColor';
import history from 'lib/history';
import { map } from 'lodash-es';
import { PanelRight } from 'lucide-react';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import { useEffect, useMemo, useState } from 'react';
import { ITraceForest, PayloadProps } from 'types/api/trace/getTraceItem';
@@ -267,14 +268,21 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
collapsed={collapsed}
reverseArrow
width={300}
collapsedWidth={40}
collapsedWidth={48}
defaultCollapsed
onCollapse={(value): void => setCollapsed(value)}
trigger={null}
data-testid="span-details-sider"
>
{!collapsed && (
<StyledCol styledclass={[styles.selectedSpanDetailContainer]}>
<StyledCol styledclass={[styles.selectedSpanDetailContainer]}>
{collapsed ? (
<Button
className="periscope-btn nav-item-label expand-collapse-btn"
icon={<PanelRight size={16} />}
onClick={(): void => setCollapsed((prev) => !prev)}
/>
) : (
<SelectedSpanDetails
setCollapsed={setCollapsed}
firstSpanStartTime={firstSpanStartTime}
traceStartTime={traceStartTime}
traceEndTime={traceEndTime}
@@ -287,8 +295,8 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
.filter(Boolean)
.find((tree) => tree)}
/>
</StyledCol>
)}
)}
</StyledCol>
</Sider>
</StyledRow>
);

View File

@@ -59,6 +59,8 @@ export const selectedSpanDetailContainer = css`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 12px;
`;
/**

View File

@@ -2,7 +2,7 @@
import type { SelectProps } from 'antd';
import { Tag, Tooltip } from 'antd';
import { BaseOptionType } from 'antd/es/select';
import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { Alerts } from 'types/api/alerts/getTriggered';
import { Container, Select } from './styles';
@@ -31,8 +31,8 @@ function TextOverflowTooltip({
}
function Filter({
setSelectedFilter,
setSelectedGroup,
onSelectedFilterChange,
onSelectedGroupChange,
allAlerts,
selectedGroup,
selectedFilter,
@@ -40,27 +40,27 @@ function Filter({
const onChangeSelectGroupHandler = useCallback(
(value: unknown) => {
if (typeof value === 'object' && Array.isArray(value)) {
setSelectedGroup(
onSelectedGroupChange(
value.map((e) => ({
value: e,
})),
);
}
},
[setSelectedGroup],
[onSelectedGroupChange],
);
const onChangeSelectedFilterHandler = useCallback(
(value: unknown) => {
if (typeof value === 'object' && Array.isArray(value)) {
setSelectedFilter(
onSelectedFilterChange(
value.map((e) => ({
value: e,
})),
);
}
},
[setSelectedFilter],
[onSelectedFilterChange],
);
const uniqueLabels: Array<string> = useMemo(() => {
@@ -122,8 +122,8 @@ function Filter({
}
interface FilterProps {
setSelectedFilter: Dispatch<SetStateAction<Array<Value>>>;
setSelectedGroup: Dispatch<SetStateAction<Array<Value>>>;
onSelectedFilterChange: (value: Array<Value>) => void;
onSelectedGroupChange: (value: Array<Value>) => void;
allAlerts: Alerts[];
selectedGroup: Array<Value>;
selectedFilter: Array<Value>;

View File

@@ -98,7 +98,7 @@ function NoFilterTable({
return (
<ResizeTable
columns={columns}
rowKey="startsAt"
rowKey={(record): string => `${record.startsAt}-${record.fingerprint}`}
dataSource={filteredAlerts}
/>
);

View File

@@ -1,4 +1,3 @@
import { useState } from 'react';
import { Alerts } from 'types/api/alerts/getTriggered';
import Filter, { Value } from './Filter';
@@ -6,18 +5,21 @@ import FilteredTable from './FilteredTable';
import NoFilterTable from './NoFilterTable';
import { NoTableContainer } from './styles';
function TriggeredAlerts({ allAlerts }: TriggeredAlertsProps): JSX.Element {
const [selectedGroup, setSelectedGroup] = useState<Value[]>([]);
const [selectedFilter, setSelectedFilter] = useState<Value[]>([]);
function TriggeredAlerts({
allAlerts,
selectedFilter,
selectedGroup,
onSelectedFilterChange,
onSelectedGroupChange,
}: TriggeredAlertsProps): JSX.Element {
return (
<div>
<Filter
allAlerts={allAlerts}
selectedFilter={selectedFilter}
selectedGroup={selectedGroup}
setSelectedFilter={setSelectedFilter}
setSelectedGroup={setSelectedGroup}
onSelectedFilterChange={onSelectedFilterChange}
onSelectedGroupChange={onSelectedGroupChange}
/>
{selectedFilter.length === 0 && selectedGroup.length === 0 ? (
@@ -45,6 +47,10 @@ function TriggeredAlerts({ allAlerts }: TriggeredAlertsProps): JSX.Element {
interface TriggeredAlertsProps {
allAlerts: Alerts[];
selectedFilter: Array<Value>;
selectedGroup: Array<Value>;
onSelectedFilterChange: (value: Array<Value>) => void;
onSelectedGroupChange: (value: Array<Value>) => void;
}
export default TriggeredAlerts;

View File

@@ -4,14 +4,17 @@ import Spinner from 'components/Spinner';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAxiosError from 'hooks/useAxiosError';
import { isUndefined } from 'lodash-es';
import { useEffect, useRef } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Value } from './Filter';
import TriggerComponent from './TriggeredAlert';
function TriggeredAlerts(): JSX.Element {
const [selectedGroup, setSelectedGroup] = useState<Value[]>([]);
const [selectedFilter, setSelectedFilter] = useState<Value[]>([]);
const userId = useSelector<AppState, string | undefined>(
(state) => state.app.user?.userId,
);
@@ -34,6 +37,14 @@ function TriggeredAlerts(): JSX.Element {
},
);
const handleSelectedFilterChange = useCallback((newFilter: Value[]) => {
setSelectedFilter(newFilter);
}, []);
const handleSelectedGroupChange = useCallback((newGroup: Value[]) => {
setSelectedGroup(newGroup);
}, []);
useEffect(() => {
if (!hasLoggedEvent.current && !isUndefined(alertsResponse.data?.payload)) {
logEvent('Alert: Triggered alert list page visited', {
@@ -44,14 +55,30 @@ function TriggeredAlerts(): JSX.Element {
}, [alertsResponse.data?.payload]);
if (alertsResponse.error) {
return <TriggerComponent allAlerts={[]} />;
return (
<TriggerComponent
allAlerts={[]}
selectedFilter={selectedFilter}
selectedGroup={selectedGroup}
onSelectedFilterChange={handleSelectedFilterChange}
onSelectedGroupChange={handleSelectedGroupChange}
/>
);
}
if (alertsResponse.isFetching || alertsResponse?.data?.payload === undefined) {
return <Spinner height="75vh" tip="Loading Alerts..." />;
}
return <TriggerComponent allAlerts={alertsResponse?.data?.payload || []} />;
return (
<TriggerComponent
allAlerts={alertsResponse?.data?.payload || []}
selectedFilter={selectedFilter}
selectedGroup={selectedGroup}
onSelectedFilterChange={handleSelectedFilterChange}
onSelectedGroupChange={handleSelectedGroupChange}
/>
);
}
export default TriggeredAlerts;

View File

@@ -32,7 +32,6 @@ export async function GetMetricQueryRange(
signal,
headers,
);
if (response.statusCode >= 400) {
let error = `API responded with ${response.statusCode} - ${response.error} status: ${response.message}`;
@@ -71,6 +70,19 @@ export async function GetMetricQueryRange(
},
);
}
if (response.payload?.data?.newResult?.data?.resultType === 'anomaly') {
response.payload.data.newResult.data.result = response.payload.data.newResult.data.result.map(
(queryData) => {
if (legendMap[queryData.queryName]) {
queryData.legend = legendMap[queryData.queryName];
}
return queryData;
},
);
}
return response;
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/no-identical-functions */
import {
MetricRangePayloadProps,
MetricRangePayloadV3,
@@ -12,8 +13,8 @@ export const convertNewDataToOld = (
result.forEach((item) => {
if (item.series) {
item.series.forEach((serie) => {
const values: QueryData['values'] = serie.values.reduce<
item.series.forEach((series) => {
const values: QueryData['values'] = series.values.reduce<
QueryData['values']
>((acc, currentInfo) => {
const renderValues: [number, string] = [
@@ -23,16 +24,87 @@ export const convertNewDataToOld = (
return [...acc, renderValues];
}, []);
const result: QueryData = {
metric: serie.labels,
metric: series.labels,
values,
queryName: item.queryName,
queryName: `${item.queryName}`,
};
oldResult.push(result);
});
}
if (item.predictedSeries) {
item.predictedSeries.forEach((series) => {
const values: QueryData['values'] = series.values.reduce<
QueryData['values']
>((acc, currentInfo) => {
const renderValues: [number, string] = [
currentInfo.timestamp / 1000,
currentInfo.value,
];
return [...acc, renderValues];
}, []);
const result: QueryData = {
metric: series.labels,
values,
queryName: `${item.queryName}`,
};
oldResult.push(result);
});
}
if (item.upperBoundSeries) {
item.upperBoundSeries.forEach((series) => {
const values: QueryData['values'] = series.values.reduce<
QueryData['values']
>((acc, currentInfo) => {
const renderValues: [number, string] = [
currentInfo.timestamp / 1000,
currentInfo.value,
];
return [...acc, renderValues];
}, []);
const result: QueryData = {
metric: series.labels,
values,
queryName: `${item.queryName}`,
};
oldResult.push(result);
});
}
if (item.lowerBoundSeries) {
item.lowerBoundSeries.forEach((series) => {
const values: QueryData['values'] = series.values.reduce<
QueryData['values']
>((acc, currentInfo) => {
const renderValues: [number, string] = [
currentInfo.timestamp / 1000,
currentInfo.value,
];
return [...acc, renderValues];
}, []);
const result: QueryData = {
metric: series.labels,
values,
queryName: `${item.queryName}`,
};
oldResult.push(result);
});
}
});
const oldResultType = resultType;
// TODO: fix it later for using only v3 version of api

View File

@@ -163,6 +163,8 @@ export const getUPlotChartOptions = ({
const stackBarChart = stackChart && isUndefined(hiddenGraph);
const isAnomalyRule = apiResponse?.data?.newResult?.data?.result[0].isAnomaly;
const series = getStackedSeries(apiResponse?.data?.result || []);
const bands = stackBarChart ? getBands(series) : null;
@@ -251,11 +253,14 @@ export const getUPlotChartOptions = ({
hooks: {
draw: [
(u): void => {
if (isAnomalyRule) {
return;
}
thresholds?.forEach((threshold) => {
if (threshold.thresholdValue !== undefined) {
const { ctx } = u;
ctx.save();
const yPos = u.valToPos(
convertValue(
threshold.thresholdValue,
@@ -265,30 +270,22 @@ export const getUPlotChartOptions = ({
'y',
true,
);
ctx.strokeStyle = threshold.thresholdColor || 'red';
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
ctx.beginPath();
const plotLeft = u.bbox.left; // left edge of the plot area
const plotRight = plotLeft + u.bbox.width; // right edge of the plot area
ctx.moveTo(plotLeft, yPos);
ctx.lineTo(plotRight, yPos);
ctx.stroke();
// Text configuration
if (threshold.thresholdLabel) {
const text = threshold.thresholdLabel;
const textX = plotRight - ctx.measureText(text).width - 20;
const canvasHeight = ctx.canvas.height;
const yposHeight = canvasHeight - yPos;
const isHeightGreaterThan90Percent = canvasHeight * 0.9 < yposHeight;
// Adjust textY based on the condition
let textY;
if (isHeightGreaterThan90Percent) {
@@ -299,7 +296,6 @@ export const getUPlotChartOptions = ({
ctx.fillStyle = threshold.thresholdColor || 'red';
ctx.fillText(text, textX, textY);
}
ctx.restore();
}
});

View File

@@ -1,3 +1,5 @@
import getLabelName from 'lib/getLabelName';
import { colors } from 'lib/getRandomColor';
import { cloneDeep, isUndefined } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData } from 'types/api/widgets/getQuery';
@@ -20,7 +22,7 @@ function getXAxisTimestamps(seriesList: QueryData[]): number[] {
function fillMissingXAxisTimestamps(timestampArr: number[], data: any[]): any {
// Generate a set of all timestamps in the range
const allTimestampsSet = new Set(timestampArr);
const processedData = JSON.parse(JSON.stringify(data));
const processedData = cloneDeep(data);
// Fill missing timestamps with null values
processedData.forEach((entry: { values: (number | null)[][] }) => {
@@ -90,3 +92,70 @@ export const getUPlotChartData = (
: yAxisValuesArr),
];
};
const processAnomalyDetectionData = (
anomalyDetectionData: any,
): Record<string, { data: number[][]; color: string }> => {
if (!anomalyDetectionData) {
return {};
}
const processedData: Record<
string,
{ data: number[][]; color: string; legendLabel: string }
> = {};
for (
let queryIndex = 0;
queryIndex < anomalyDetectionData.length;
queryIndex++
) {
const {
series,
predictedSeries,
upperBoundSeries,
lowerBoundSeries,
queryName,
legend,
} = anomalyDetectionData[queryIndex];
for (let index = 0; index < series?.length; index++) {
const label = getLabelName(
series[index].labels,
queryName || '', // query
legend || '',
);
const objKey = `${queryName}-${label}`;
processedData[objKey] = {
data: [
series[index].values.map((v: { timestamp: number }) => v.timestamp / 1000),
series[index].values.map((v: { value: number }) => v.value),
predictedSeries[index].values.map((v: { value: number }) => v.value),
upperBoundSeries[index].values.map((v: { value: number }) => v.value),
lowerBoundSeries[index].values.map((v: { value: number }) => v.value),
],
color: colors[index],
legendLabel: label,
};
}
}
return processedData;
};
export const getUplotChartDataForAnomalyDetection = (
apiResponse?: MetricRangePayloadProps,
): Record<
string,
{
[x: string]: any;
data: number[][];
color: string;
}
> => {
const anomalyDetectionData = apiResponse?.data?.newResult?.data?.result;
return processAnomalyDetectionData(anomalyDetectionData);
};

View File

@@ -233,6 +233,43 @@ GetYAxisScale): { auto?: boolean; range?: uPlot.Scale.Range } => {
return { auto: false, range: [min, max] };
};
function getMinMax(data: any): { minValue: number; maxValue: number } {
// Exclude the first array
const arrays = data.slice(1);
// Flatten the array and convert all elements to float
const flattened = arrays.flat().map(Number);
// Get min and max, with fallback of 0 for min
const minValue = flattened.length ? Math.min(...flattened) : 0;
const maxValue = Math.max(...flattened);
return { minValue, maxValue };
}
export const getYAxisScaleForAnomalyDetection = ({
seriesData,
selectedSeries,
initialData,
}: {
seriesData: any;
selectedSeries: string | null;
initialData: any;
yAxisUnit?: string;
}): { auto?: boolean; range?: uPlot.Scale.Range } => {
if (!selectedSeries && !initialData) {
return { auto: true };
}
const selectedSeriesData = selectedSeries
? seriesData[selectedSeries]?.data
: initialData;
const { minValue, maxValue } = getMinMax(selectedSeriesData);
return { auto: false, range: [minValue, maxValue] };
};
export type GetYAxisScale = {
thresholds?: ThresholdProps[];
series?: QueryDataV3[];

View File

@@ -15,7 +15,7 @@ import {
} from 'pages/AlertDetails/hooks';
import CopyToClipboard from 'periscope/components/CopyToClipboard';
import { useAlertRule } from 'providers/Alert';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { CSSProperties } from 'styled-components';
import { AlertDef } from 'types/api/alerts/def';
@@ -32,7 +32,7 @@ function AlertActionButtons({
ruleId: string;
alertDetails: AlertHeaderProps['alertDetails'];
}): JSX.Element {
const { isAlertRuleDisabled } = useAlertRule();
const { alertRuleState, setAlertRuleState } = useAlertRule();
const { handleAlertStateToggle } = useAlertRuleStatusToggle({ ruleId });
const { handleAlertDuplicate } = useAlertRuleDuplicate({
@@ -79,13 +79,32 @@ function AlertActionButtons({
);
const isDarkMode = useIsDarkMode();
// state for immediate UI feedback rather than waiting for onSuccess of handleAlertStateTiggle to updating the alertRuleState
const [isAlertRuleDisabled, setIsAlertRuleDisabled] = useState<
undefined | boolean
>(undefined);
useEffect(() => {
if (alertRuleState === undefined) {
setAlertRuleState(alertDetails.state);
setIsAlertRuleDisabled(alertDetails.state === 'disabled');
}
}, [setAlertRuleState, alertRuleState, alertDetails.state]);
// on unmount remove the alert state
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => (): void => setAlertRuleState(undefined), []);
return (
<div className="alert-action-buttons">
<Tooltip title={isAlertRuleDisabled ? 'Enable alert' : 'Disable alert'}>
<Tooltip title={alertRuleState ? 'Enable alert' : 'Disable alert'}>
{isAlertRuleDisabled !== undefined && (
<Switch
size="small"
onChange={handleAlertStateToggle}
onChange={(): void => {
setIsAlertRuleDisabled((prev) => !prev);
handleAlertStateToggle();
}}
checked={!isAlertRuleDisabled}
/>
)}

View File

@@ -2,7 +2,7 @@ import './AlertHeader.styles.scss';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { useAlertRule } from 'providers/Alert';
import { useEffect, useMemo } from 'react';
import { useMemo } from 'react';
import AlertActionButtons from './ActionButtons/ActionButtons';
import AlertLabels from './AlertLabels/AlertLabels';
@@ -19,7 +19,7 @@ export type AlertHeaderProps = {
};
};
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
const { state, alert, labels, disabled } = alertDetails;
const { state, alert, labels } = alertDetails;
const labelsWithoutSeverity = useMemo(
() =>
@@ -29,20 +29,14 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
[labels],
);
const { isAlertRuleDisabled, setIsAlertRuleDisabled } = useAlertRule();
useEffect(() => {
if (isAlertRuleDisabled === undefined) {
setIsAlertRuleDisabled(disabled);
}
}, [disabled, setIsAlertRuleDisabled, isAlertRuleDisabled]);
const { alertRuleState } = useAlertRule();
return (
<div className="alert-info">
<div className="alert-info__info-wrapper">
<div className="top-section">
<div className="alert-title-wrapper">
<AlertState state={isAlertRuleDisabled ? 'disabled' : state} />
<AlertState state={alertRuleState ?? state} />
<div className="alert-title">
<LineClampedText text={alert} />
</div>

View File

@@ -4,7 +4,7 @@ import KeyValueLabel from 'periscope/components/KeyValueLabel';
import SeeMore from 'periscope/components/SeeMore';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AlertLabelsProps = {
export type AlertLabelsProps = {
labels: Record<string, any>;
initialCount?: number;
};

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