mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 02:48:59 +08:00
Merge remote-tracking branch 'upstream/develop' into feat/logs
This commit is contained in:
commit
df17d4ca54
22
.github/workflows/dependency-review.yml
vendored
Normal file
22
.github/workflows/dependency-review.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
with:
|
||||
fail-on-severity: high
|
||||
uses: actions/dependency-review-action@v2
|
406
CONTRIBUTING.md
406
CONTRIBUTING.md
@ -1,122 +1,331 @@
|
||||
# How to Contribute
|
||||
# Contributing Guidelines
|
||||
|
||||
There are primarily 2 areas in which you can contribute in SigNoz
|
||||
## Welcome to SigNoz Contributing section 🎉
|
||||
|
||||
- Frontend ( written in Typescript, React)
|
||||
- Backend - ( Query Service - written in Go)
|
||||
Hi there! We're thrilled that you'd like to contribute to this project, thank you for your interest. Whether it's a bug report, new feature, correction, or additional documentation, we greatly value feedback and contributions from our community.
|
||||
|
||||
Depending upon your area of expertise & interest, you can chose one or more to contribute. Below are detailed instructions to contribute in each area
|
||||
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution.
|
||||
|
||||
> Please note: If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻
|
||||
- We accept contributions made to the [SigNoz `develop` branch]()
|
||||
- Find all SigNoz Docker Hub images here
|
||||
- [signoz/frontend](https://hub.docker.com/r/signoz/frontend)
|
||||
- [signoz/query-service](https://hub.docker.com/r/signoz/query-service)
|
||||
- [signoz/otelcontribcol](https://hub.docker.com/r/signoz/otelcontribcol)
|
||||
|
||||
> If you just raise a PR, without the corresponding issue being assigned to you - it may not be accepted.
|
||||
## Finding contributions to work on 💬
|
||||
|
||||
# Develop Frontend
|
||||
Looking at the existing issues is a great way to find something to contribute on.
|
||||
Also, have a look at these [good first issues label](https://github.com/SigNoz/signoz/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to start with.
|
||||
|
||||
Need to update [https://github.com/SigNoz/signoz/tree/main/frontend](https://github.com/SigNoz/signoz/tree/main/frontend)
|
||||
|
||||
### Contribute to Frontend with Docker installation of SigNoz
|
||||
## Sections:
|
||||
- [General Instructions](#1-general-instructions-)
|
||||
- [For Creating Issue(s)](#11-for-creating-issues)
|
||||
- [For Pull Requests(s)](#12-for-pull-requests)
|
||||
- [How to Contribute](#2-how-to-contribute-%EF%B8%8F)
|
||||
- [Develop Frontend](#3-develop-frontend-)
|
||||
- [Contribute to Frontend with Docker installation of SigNoz](#31-contribute-to-frontend-with-docker-installation-of-signoz)
|
||||
- [Contribute to Frontend without installing SigNoz backend](#32-contribute-to-frontend-without-installing-signoz-backend)
|
||||
- [Contribute to Backend (Query-Service)](#4-contribute-to-backend-query-service-)
|
||||
- [To run ClickHouse setup](#41-to-run-clickhouse-setup-recommended-for-local-development)
|
||||
- [Contribute to SigNoz Helm Chart](#5-contribute-to-signoz-helm-chart-)
|
||||
- [To run helm chart for local development](#51-to-run-helm-chart-for-local-development)
|
||||
- [Other Ways to Contribute](#other-ways-to-contribute)
|
||||
|
||||
- `git clone https://github.com/SigNoz/signoz.git && cd signoz`
|
||||
- comment out frontend service section at `deploy/docker/clickhouse-setup/docker-compose.yaml#L62`
|
||||
- run `cd deploy` to move to deploy directory
|
||||
- Install signoz locally without the frontend
|
||||
- Add below configuration to query-service section at `docker/clickhouse-setup/docker-compose.yaml#L38`
|
||||
# 1. General Instructions 📝
|
||||
|
||||
```docker
|
||||
## 1.1 For Creating Issue(s)
|
||||
Before making any significant changes and before filing a new issue, please check [existing open](https://github.com/SigNoz/signoz/issues?q=is%3Aopen+is%3Aissue), or [recently closed](https://github.com/SigNoz/signoz/issues?q=is%3Aissue+is%3Aclosed) issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can.
|
||||
|
||||
**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy)
|
||||
|
||||
#### Details like these are incredibly useful:
|
||||
|
||||
- **Requirement** - what kind of use case are you trying to solve?
|
||||
- **Proposal** - what do you suggest to solve the problem or improve the existing
|
||||
situation?
|
||||
- Any open questions to address❓
|
||||
|
||||
#### If you are reporting a bug, details like these are incredibly useful:
|
||||
|
||||
- A reproducible test case or series of steps.
|
||||
- The version of our code being used.
|
||||
- Any modifications you've made relevant to the bug🐞.
|
||||
- Anything unusual about your environment or deployment.
|
||||
|
||||
Discussing your proposed changes ahead of time will make the contribution
|
||||
process smooth for everyone 🙌.
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
<hr>
|
||||
|
||||
## 1.2 For Pull Request(s)
|
||||
|
||||
Contributions via pull requests are much appreciated. Once the approach is agreed upon ✅, make your changes and open a Pull Request(s).
|
||||
Before sending us a pull request, please ensure that,
|
||||
|
||||
- Fork the SigNoz repo on GitHub, clone it on your machine.
|
||||
- Create a branch with your changes.
|
||||
- You are working against the latest source on the `develop` branch.
|
||||
- Modify the source; please focus only on the specific change you are contributing.
|
||||
- Ensure local tests pass.
|
||||
- Commit to your fork using clear commit messages.
|
||||
- Send us a pull request, answering any default questions in the pull request interface.
|
||||
- Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation
|
||||
- Once you've pushed your commits to GitHub, make sure that your branch can be auto-merged (there are no merge conflicts). If not, on your computer, merge main into your branch, resolve any merge conflicts, make sure everything still runs correctly and passes all the tests, and then push up those changes.
|
||||
- Once the change has been approved and merged, we will inform you in a comment.
|
||||
|
||||
|
||||
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
|
||||
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
|
||||
|
||||
**Note:** Unless your change is small, **please** consider submitting different Pull Rrequest(s):
|
||||
|
||||
* 1️⃣ First PR should include the overall structure of the new component:
|
||||
* Readme, configuration, interfaces or base classes, etc...
|
||||
* This PR is usually trivial to review, so the size limit does not apply to
|
||||
it.
|
||||
* 2️⃣ Second PR should include the concrete implementation of the component. If the
|
||||
size of this PR is larger than the recommended size, consider **splitting** ⚔️ it into
|
||||
multiple PRs.
|
||||
* If there are multiple sub-component then ideally each one should be implemented as
|
||||
a **separate** pull request.
|
||||
* Last PR should include changes to **any user-facing documentation.** And should include
|
||||
end-to-end tests if applicable. The component must be enabled
|
||||
only after sufficient testing, and there is enough confidence in the
|
||||
stability and quality of the component.
|
||||
|
||||
|
||||
You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [SLACK](https://signoz.io/slack).
|
||||
|
||||
### Pointers:
|
||||
- If you find any **bugs** → please create an [**issue.**](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=)
|
||||
- If you find anything **missing** in documentation → you can create an issue with the label **`documentation`**.
|
||||
- If you want to build any **new feature** → please create an [issue with the label **`enhancement`**.](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=)
|
||||
- If you want to **discuss** something about the product, start a new [**discussion**.](https://github.com/SigNoz/signoz/discussions)
|
||||
|
||||
<hr>
|
||||
|
||||
### Conventions to follow when submitting Commits and Pull Request(s).
|
||||
|
||||
We try to follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), more specifically the commits and PRs **should have type specifiers** prefixed in the name. [This](https://www.conventionalcommits.org/en/v1.0.0/#specification) should give you a better idea.
|
||||
|
||||
e.g. If you are submitting a fix for an issue in frontend, the PR name should be prefixed with **`fix(FE):`**
|
||||
|
||||
- Follow [GitHub Flow](https://guides.github.com/introduction/flow/) guidelines for your contribution flows.
|
||||
|
||||
- Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :)
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
<hr>
|
||||
|
||||
# 2. How to Contribute 🙋🏻♂️
|
||||
|
||||
#### There are primarily 2 areas in which you can contribute to SigNoz
|
||||
|
||||
- [**Frontend**](#3-develop-frontend-) (Written in Typescript, React)
|
||||
- [**Backend**](#4-contribute-to-backend-query-service-) (Query Service, written in Go)
|
||||
|
||||
Depending upon your area of expertise & interest, you can choose one or more to contribute. Below are detailed instructions to contribute in each area.
|
||||
|
||||
**Please note:** If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻
|
||||
|
||||
⚠️ If you just raise a PR, without the corresponding issue being assigned to you - it may not be accepted.
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
<hr>
|
||||
|
||||
# 3. Develop Frontend 🌚
|
||||
|
||||
**Need to Update: [https://github.com/SigNoz/signoz/tree/develop/frontend](https://github.com/SigNoz/signoz/tree/develop/frontend)**
|
||||
|
||||
Also, have a look at [Frontend README.md](https://github.com/SigNoz/signoz/blob/develop/frontend/README.md) sections for more info on how to setup SigNoz frontend locally (with and without Docker).
|
||||
|
||||
## 3.1 Contribute to Frontend with Docker installation of SigNoz
|
||||
|
||||
- Clone the SigNoz repository and cd into signoz directory,
|
||||
```
|
||||
git clone https://github.com/SigNoz/signoz.git && cd signoz
|
||||
```
|
||||
- Comment out `frontend` service section at [`deploy/docker/clickhouse-setup/docker-compose.yaml#L68`](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml#L68)
|
||||
|
||||

|
||||
|
||||
|
||||
- run `cd deploy` to move to deploy directory,
|
||||
- Install signoz locally **without** the frontend,
|
||||
- Add / Uncomment the below configuration to query-service section at [`deploy/docker/clickhouse-setup/docker-compose.yaml#L47`](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml#L47)
|
||||
```
|
||||
ports:
|
||||
- "8080:8080"
|
||||
```
|
||||
- If you are using x86_64 processors (All Intel/AMD processors) run `sudo docker-compose -f docker/clickhouse-setup/docker-compose.yaml up -d`
|
||||
- If you are on arm64 processors (Apple M1 Macbooks) run `sudo docker-compose -f docker/clickhouse-setup/docker-compose.arm.yaml up -d`
|
||||
- `cd ../frontend` and change baseURL to `http://localhost:8080` in file `src/constants/env.ts`
|
||||
- `yarn install`
|
||||
- `yarn dev`
|
||||
<img width="869" alt="query service" src="https://user-images.githubusercontent.com/52788043/179010251-8489be31-04ca-42f8-b30d-ef0bb6accb6b.png">
|
||||
|
||||
- Next run,
|
||||
```
|
||||
sudo docker-compose -f docker/clickhouse-setup/docker-compose.yaml up -d
|
||||
```
|
||||
- `cd ../frontend` and change baseURL in file [`frontend/src/constants/env.ts#L2`](https://github.com/SigNoz/signoz/blob/develop/frontend/src/constants/env.ts#L2) and for that, you need to create a `.env` file in the `frontend` directory with the following environment variable (`FRONTEND_API_ENDPOINT`) matching your configuration.
|
||||
|
||||
> Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh`
|
||||
If you have backend api exposed via frontend nginx:
|
||||
```
|
||||
FRONTEND_API_ENDPOINT=http://localhost:3301
|
||||
```
|
||||
If not:
|
||||
```
|
||||
FRONTEND_API_ENDPOINT=http://localhost:8080
|
||||
```
|
||||
|
||||
### Contribute to Frontend without installing SigNoz backend
|
||||
- Next,
|
||||
```
|
||||
yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
||||
If you don't want to install SigNoz backend just for doing frontend development, we can provide you with test environments which you can use as the backend. Please ping us in #contributing channel in our [slack community](https://signoz.io/slack) and we will DM you with `<test environment URL>`
|
||||
### Important Notes:
|
||||
The Maintainers / Contributors who will change Line Numbers of `Frontend` & `Query-Section`, please update line numbers in [`/.scripts/commentLinesForSetup.sh`](https://github.com/SigNoz/signoz/blob/develop/.scripts/commentLinesForSetup.sh)
|
||||
|
||||
- `git clone https://github.com/SigNoz/signoz.git && cd signoz/frontend`
|
||||
- Create a file `.env` with `FRONTEND_API_ENDPOINT=<test environment URL>`
|
||||
- `yarn install`
|
||||
- `yarn dev`
|
||||
**[`^top^`](#)**
|
||||
|
||||
**_Frontend should now be accessible at `http://localhost:3301/application`_**
|
||||
## 3.2 Contribute to Frontend without installing SigNoz backend
|
||||
|
||||
# Contribute to Query-Service
|
||||
If you don't want to install the SigNoz backend just for doing frontend development, we can provide you with test environments that you can use as the backend.
|
||||
|
||||
Need to update [https://github.com/SigNoz/signoz/tree/main/pkg/query-service](https://github.com/SigNoz/signoz/tree/main/pkg/query-service)
|
||||
- Clone the SigNoz repository and cd into signoz/frontend directory,
|
||||
```
|
||||
git clone https://github.com/SigNoz/signoz.git && cd signoz/frontend
|
||||
````
|
||||
- Create a file `.env` in the `frontend` directory with `FRONTEND_API_ENDPOINT=<test environment URL>`
|
||||
- Next,
|
||||
```
|
||||
yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### To run ClickHouse setup (recommended for local development)
|
||||
Please ping us in the [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) channel or ask `@Prashant Shahi` in our [Slack Community](https://signoz.io/slack) and we will DM you with `<test environment URL>`.
|
||||
|
||||
- git clone https://github.com/SigNoz/signoz.git
|
||||
- run `cd signoz` to move to signoz directory
|
||||
- run `sudo make dev-setup` to configure local setup to run query-service
|
||||
- comment out frontend service section at `docker/clickhouse-setup/docker-compose.yaml`
|
||||
- comment out query-service section at `docker/clickhouse-setup/docker-compose.yaml`
|
||||
- add below configuration to clickhouse section at `docker/clickhouse-setup/docker-compose.yaml`
|
||||
```docker
|
||||
expose:
|
||||
- 9000
|
||||
ports:
|
||||
- 9001:9000
|
||||
**Frontend should now be accessible at** [`http://localhost:3301/application`](http://localhost:3301/application)
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
<hr>
|
||||
|
||||
# 4. Contribute to Backend (Query-Service) 🌑
|
||||
|
||||
**Need to Update:** [**https://github.com/SigNoz/signoz/tree/develop/pkg/query-service**](https://github.com/SigNoz/signoz/tree/develop/pkg/query-service)
|
||||
|
||||
## 4.1 To run ClickHouse setup (recommended for local development)
|
||||
|
||||
- Clone the SigNoz repository and cd into signoz directory,
|
||||
```
|
||||
git clone https://github.com/SigNoz/signoz.git && cd signoz
|
||||
```
|
||||
- run `sudo make dev-setup` to configure local setup to run query-service,
|
||||
- Comment out `frontend` service section at [`deploy/docker/clickhouse-setup/docker-compose.yaml#L68`](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml#L68)
|
||||
<img width="982" alt="develop-frontend" src="https://user-images.githubusercontent.com/52788043/179043977-012be8b0-a2ed-40d1-b2e6-2ab72d7989c0.png">
|
||||
|
||||
- Comment out `query-service` section at [`deploy/docker/clickhouse-setup/docker-compose.yaml#L41`,](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml#L41)
|
||||
<img width="1068" alt="Screenshot 2022-07-14 at 22 48 07" src="https://user-images.githubusercontent.com/52788043/179044151-a65ba571-db0b-4a16-b64b-ca3fadcf3af0.png">
|
||||
|
||||
- add below configuration to `clickhouse` section at [`deploy/docker/clickhouse-setup/docker-compose.yaml`,](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml)
|
||||
```
|
||||
ports:
|
||||
- 9001:9000
|
||||
```
|
||||
<img width="1013" alt="Screenshot 2022-07-14 at 22 50 37" src="https://user-images.githubusercontent.com/52788043/179044544-a293d3bc-4c4f-49ea-a276-505a381de67d.png">
|
||||
|
||||
- run `cd pkg/query-service/` to move to `query-service` directory,
|
||||
- Then, you need to create a `.env` file with the following environment variable
|
||||
```
|
||||
SIGNOZ_LOCAL_DB_PATH="./signoz.db"
|
||||
```
|
||||
to set your local environment with the right `RELATIONAL_DATASOURCE_PATH` as mentioned in [`./constants/constants.go#L38`,](https://github.com/SigNoz/signoz/blob/develop/pkg/query-service/constants/constants.go#L38)
|
||||
|
||||
- Now, install SigNoz locally **without** the `frontend` and `query-service`,
|
||||
- If you are using `x86_64` processors (All Intel/AMD processors) run `sudo make run-x86`
|
||||
- If you are on `arm64` processors (Apple M1 Macs) run `sudo make run-arm`
|
||||
|
||||
#### Run locally,
|
||||
```
|
||||
|
||||
- run `cd pkg/query-service/` to move to query-service directory
|
||||
- Open ./constants/constants.go
|
||||
- Replace ```const RELATIONAL_DATASOURCE_PATH = "/var/lib/signoz/signoz.db"``` \
|
||||
with ```const RELATIONAL_DATASOURCE_PATH = "./signoz.db".```
|
||||
|
||||
- Install signoz locally without the frontend and query-service
|
||||
- If you are using x86_64 processors (All Intel/AMD processors) run `sudo make run-x86`
|
||||
- If you are on arm64 processors (Apple M1 Macbooks) run `sudo make run-arm`
|
||||
|
||||
#### Run locally
|
||||
```console
|
||||
ClickHouseUrl=tcp://localhost:9001 STORAGE=clickhouse go run main.go
|
||||
```
|
||||
|
||||
> Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh`
|
||||
#### Build and Run locally
|
||||
```
|
||||
cd pkg/query-service
|
||||
go build -o build/query-service main.go
|
||||
ClickHouseUrl=tcp://localhost:9001 STORAGE=clickhouse build/query-service
|
||||
```
|
||||
|
||||
**_Query Service should now be available at `http://localhost:8080`_**
|
||||
#### Docker Images
|
||||
The docker images of query-service is available at https://hub.docker.com/r/signoz/query-service
|
||||
|
||||
> If you want to see how, frontend plays with query service, you can run frontend also in you local env with the baseURL changed to `http://localhost:8080` in file `src/constants/env.ts` as the query-service is now running at port `8080`
|
||||
```
|
||||
docker pull signoz/query-service
|
||||
```
|
||||
|
||||
```
|
||||
docker pull signoz/query-service:latest
|
||||
```
|
||||
|
||||
```
|
||||
docker pull signoz/query-service:develop
|
||||
```
|
||||
|
||||
### Important Note:
|
||||
The Maintainers / Contributors who will change Line Numbers of `Frontend` & `Query-Section`, please update line numbers in [`/.scripts/commentLinesForSetup.sh`](https://github.com/SigNoz/signoz/blob/develop/.scripts/commentLinesForSetup.sh)
|
||||
|
||||
|
||||
|
||||
**Query Service should now be available at** [`http://localhost:8080`](http://localhost:8080)
|
||||
|
||||
If you want to see how the frontend plays with query service, you can run the frontend also in your local env with the baseURL changed to `http://localhost:8080` in file [`frontend/src/constants/env.ts`](https://github.com/SigNoz/signoz/blob/develop/frontend/src/constants/env.ts) as the `query-service` is now running at port `8080`.
|
||||
|
||||
---
|
||||
<!-- Instead of configuring a local setup, you can also use [Gitpod](https://www.gitpod.io/), a VSCode-based Web IDE.
|
||||
|
||||
Click the button below. A workspace with all required environments will be created.
|
||||
|
||||
[](https://gitpod.io/#https://github.com/SigNoz/signoz)
|
||||
|
||||
> To use it on your forked repo, edit the 'Open in Gitpod' button url to `https://gitpod.io/#https://github.com/<your-github-username>/signoz` -->
|
||||
> To use it on your forked repo, edit the 'Open in Gitpod' button URL to `https://gitpod.io/#https://github.com/<your-github-username>/signoz` -->
|
||||
|
||||
# Contribute to SigNoz Helm Chart
|
||||
**[`^top^`](#)**
|
||||
|
||||
<hr>
|
||||
|
||||
Need to update [https://github.com/SigNoz/charts](https://github.com/SigNoz/charts).
|
||||
# 5. Contribute to SigNoz Helm Chart 📊
|
||||
|
||||
### To run helm chart for local development
|
||||
**Need to Update: [https://github.com/SigNoz/charts](https://github.com/SigNoz/charts).**
|
||||
|
||||
- run `git clone https://github.com/SigNoz/charts.git` followed by `cd charts`
|
||||
- it is recommended to use lightweight kubernetes (k8s) cluster for local development:
|
||||
## 5.1 To run helm chart for local development
|
||||
|
||||
- Clone the SigNoz repository and cd into charts directory,
|
||||
```
|
||||
git clone https://github.com/SigNoz/charts.git && cd charts
|
||||
```
|
||||
- It is recommended to use lightweight kubernetes (k8s) cluster for local development:
|
||||
- [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation)
|
||||
- [k3d](https://k3d.io/#installation)
|
||||
- [minikube](https://minikube.sigs.k8s.io/docs/start/)
|
||||
- create a k8s cluster and make sure `kubectl` points to the locally created k8s cluster
|
||||
- run `make dev-install` to install SigNoz chart with `my-release` release name in `platform` namespace.
|
||||
- run `kubectl -n platform port-forward svc/my-release-signoz-frontend 3301:3301` to make SigNoz UI available at [localhost:3301](http://localhost:3301)
|
||||
- create a k8s cluster and make sure `kubectl` points to the locally created k8s cluster,
|
||||
- run `make dev-install` to install SigNoz chart with `my-release` release name in `platform` namespace,
|
||||
- next run,
|
||||
```
|
||||
kubectl -n platform port-forward svc/my-release-signoz-frontend 3301:3301
|
||||
```
|
||||
to make SigNoz UI available at [localhost:3301](http://localhost:3301)
|
||||
|
||||
**To install HotROD sample app:**
|
||||
**5.1.1 To install the HotROD sample app:**
|
||||
|
||||
```bash
|
||||
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-install.sh \
|
||||
| HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash
|
||||
```
|
||||
|
||||
**To load data with HotROD sample app:**
|
||||
**5.1.2 To load data with the HotROD sample app:**
|
||||
|
||||
```bash
|
||||
kubectl -n sample-application run strzal --image=djbingham/curl \
|
||||
@ -124,7 +333,7 @@ kubectl -n sample-application run strzal --image=djbingham/curl \
|
||||
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
|
||||
```
|
||||
|
||||
**To stop the load generation:**
|
||||
**5.1.3 To stop the load generation:**
|
||||
|
||||
```bash
|
||||
kubectl -n sample-application run strzal --image=djbingham/curl \
|
||||
@ -132,59 +341,32 @@ kubectl -n sample-application run strzal --image=djbingham/curl \
|
||||
http://locust-master:8089/stop
|
||||
```
|
||||
|
||||
**To delete HotROD sample app:**
|
||||
**5.1.4 To delete the HotROD sample app:**
|
||||
|
||||
```bash
|
||||
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-delete.sh \
|
||||
| HOTROD_NAMESPACE=sample-application bash
|
||||
```
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
---
|
||||
|
||||
## General Instructions
|
||||
## Other Ways to Contribute
|
||||
|
||||
**Before making any significant changes, please open an issue**. Each issue
|
||||
should describe the following:
|
||||
There are many other ways to get involved with the community and to participate in this project:
|
||||
|
||||
* Requirement - what kind of use case are you trying to solve?
|
||||
* Proposal - what do you suggest to solve the problem or improve the existing
|
||||
situation?
|
||||
* Any open questions to address
|
||||
|
||||
Discussing your proposed changes ahead of time will make the contribution
|
||||
process smooth for everyone. Once the approach is agreed upon, make your changes
|
||||
and open a pull request(s). Unless your change is small, Please consider submitting different PRs:
|
||||
|
||||
* First PR should include the overall structure of the new component:
|
||||
* Readme, configuration, interfaces or base classes etc...
|
||||
* This PR is usually trivial to review, so the size limit does not apply to
|
||||
it.
|
||||
* Second PR should include the concrete implementation of the component. If the
|
||||
size of this PR is larger than the recommended size consider splitting it in
|
||||
multiple PRs.
|
||||
* If there are multiple sub-component then ideally each one should be implemented as
|
||||
a separate pull request.
|
||||
* Last PR should include changes to any user facing documentation. And should include
|
||||
end to end tests if applicable. The component must be enabled
|
||||
only after sufficient testing, and there is enough confidence in the
|
||||
stability and quality of the component.
|
||||
- Use the product, submitting GitHub issues when a problem is found.
|
||||
- Help code review pull requests and participate in issue threads.
|
||||
- Submit a new feature request as an issue.
|
||||
- Help answer questions on forums such as Stack Overflow and [SigNoz Community Slack Channel](https://signoz.io/slack).
|
||||
- Tell others about the project on Twitter, your blog, etc.
|
||||
|
||||
|
||||
You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [slack](https://signoz.io/slack).
|
||||
## License
|
||||
|
||||
- If you find any bugs, please create an issue
|
||||
- If you find anything missing in documentation, you can create an issue with label **documentation**
|
||||
- If you want to build any new feature, please create an issue with label `enhancement`
|
||||
- If you want to discuss something about the product, start a new [discussion](https://github.com/SigNoz/signoz/discussions)
|
||||
By contributing to SigNoz, you agree that your contributions will be licensed under its MIT license.
|
||||
|
||||
### Conventions to follow when submitting commits, PRs
|
||||
Again, Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :)
|
||||
|
||||
1. We try to follow https://www.conventionalcommits.org/en/v1.0.0/
|
||||
|
||||
More specifically the commits and PRs should have type specifiers prefixed in the name. [This](https://www.conventionalcommits.org/en/v1.0.0/#specification) should give you a better idea.
|
||||
|
||||
e.g. If you are submitting a fix for an issue in frontend - PR name should be prefixed with `fix(FE):`
|
||||
|
||||
2. Follow [GitHub Flow](https://guides.github.com/introduction/flow/) guidelines for your contribution flows
|
||||
|
||||
3. Feel free to ping us on `#contributing` or `#contributing-frontend` on our slack community if you need any help on this :)
|
||||
Thank You!
|
||||
|
6
Makefile
6
Makefile
@ -82,15 +82,9 @@ dev-setup:
|
||||
run-x86:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up -d
|
||||
|
||||
run-arm:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.arm.yaml up -d
|
||||
|
||||
down-x86:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v
|
||||
|
||||
down-arm:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.arm.yaml down -v
|
||||
|
||||
clear-standalone-data:
|
||||
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
|
||||
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse/* signoz/*"
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
|
||||
-->
|
||||
<level>trace</level>
|
||||
<level>information</level>
|
||||
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
|
||||
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
|
||||
<!-- Rotation policy
|
||||
|
@ -40,7 +40,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.9.2
|
||||
image: signoz/query-service:0.10.0
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@ -68,7 +68,7 @@ services:
|
||||
- clickhouse
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.9.2
|
||||
image: signoz/frontend:0.10.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@ -81,20 +81,24 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/otelcontribcol:0.45.1-1.0
|
||||
image: signoz/otelcontribcol:0.45.1-1.1
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
# - "8889:8889" # Prometheus metrics exposed by the agent
|
||||
# - "13133:13133" # health_check
|
||||
# - "14268:14268" # Jaeger 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
|
||||
# - "55680:55680" # OTLP gRPC legacy receiver
|
||||
# - "55681:55681" # OTLP HTTP legacy receiver
|
||||
# - "55679:55679" # zPages extension
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 3
|
||||
@ -107,10 +111,15 @@ services:
|
||||
- clickhouse
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/otelcontribcol:0.45.1-1.0
|
||||
image: signoz/otelcontribcol:0.45.1-1.1
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "13133:13133" # Health check extension
|
||||
# - "55679:55679" # zPages extension
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
@ -1,30 +1,46 @@
|
||||
receivers:
|
||||
opencensus:
|
||||
endpoint: 0.0.0.0:55678
|
||||
otlp/spanmetrics:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: "localhost:12345"
|
||||
endpoint: localhost:12345
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
jaeger:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:14250
|
||||
thrift_http:
|
||||
endpoint: 0.0.0.0:14268
|
||||
# thrift_compact:
|
||||
# endpoint: 0.0.0.0:6831
|
||||
# thrift_binary:
|
||||
# endpoint: 0.0.0.0:6832
|
||||
hostmetrics:
|
||||
collection_interval: 60s
|
||||
scrapers:
|
||||
cpu:
|
||||
load:
|
||||
memory:
|
||||
disk:
|
||||
filesystem:
|
||||
network:
|
||||
cpu: {}
|
||||
load: {}
|
||||
memory: {}
|
||||
disk: {}
|
||||
filesystem: {}
|
||||
network: {}
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
send_batch_max_size: 11000
|
||||
timeout: 10s
|
||||
resourcedetection:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
override: false
|
||||
signozspanmetrics/prometheus:
|
||||
metrics_exporter: prometheus
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
@ -49,9 +65,7 @@ processors:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
extensions:
|
||||
health_check: {}
|
||||
zpages: {}
|
||||
|
||||
exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
@ -60,18 +74,35 @@ exporters:
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
prometheus:
|
||||
endpoint: "0.0.0.0:8889"
|
||||
endpoint: 0.0.0.0:8889
|
||||
# logging: {}
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
service:
|
||||
extensions: [health_check, zpages]
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions: [health_check, zpages, pprof]
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [jaeger, otlp]
|
||||
processors: [signozspanmetrics/prometheus, batch]
|
||||
exporters: [clickhousetraces]
|
||||
metrics:
|
||||
receivers: [otlp, hostmetrics]
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/hostmetrics:
|
||||
receivers: [hostmetrics]
|
||||
processors: [resourcedetection, batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/spanmetrics:
|
||||
receivers: [otlp/spanmetrics]
|
||||
exporters: [prometheus]
|
||||
exporters: [prometheus]
|
||||
|
@ -1,17 +1,26 @@
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
http:
|
||||
|
||||
# Data sources: metrics
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: "otel-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets: ["otel-collector:8889"]
|
||||
- targets:
|
||||
- otel-collector:8888
|
||||
# otel-collector-metrics internal metrics
|
||||
- job_name: "otel-collector-metrics"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
# SigNoz span metrics
|
||||
- job_name: "signozspanmetrics-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- otel-collector:8889
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
@ -32,17 +41,26 @@ processors:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
extensions:
|
||||
health_check: {}
|
||||
zpages: {}
|
||||
|
||||
exporters:
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
service:
|
||||
extensions: [health_check, zpages]
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions: [health_check, zpages, pprof]
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp, prometheus]
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
exporters: [clickhousemetricswrite]
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
|
||||
-->
|
||||
<level>trace</level>
|
||||
<level>information</level>
|
||||
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
|
||||
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
|
||||
<!-- Rotation policy
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -39,7 +39,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.9.2
|
||||
image: signoz/query-service:0.10.0
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
@ -66,7 +66,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.9.2
|
||||
image: signoz/frontend:0.10.0
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@ -78,20 +78,24 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/otelcontribcol:0.45.1-1.0
|
||||
image: signoz/otelcontribcol:0.45.1-1.1
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
# - "8889:8889" # Prometheus metrics exposed by the agent
|
||||
# - "13133:13133" # health_check
|
||||
# - "14268:14268" # Jaeger 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
|
||||
# - "55680:55680" # OTLP gRPC legacy receiver
|
||||
# - "55681:55681" # OTLP HTTP legacy receiver
|
||||
# - "55679:55679" # zPages extension
|
||||
mem_limit: 2000m
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@ -99,10 +103,15 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/otelcontribcol:0.45.1-1.0
|
||||
image: signoz/otelcontribcol:0.45.1-1.1
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "13133:13133" # Health check extension
|
||||
# - "55679:55679" # zPages extension
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
clickhouse:
|
||||
|
@ -1,25 +1,36 @@
|
||||
receivers:
|
||||
opencensus:
|
||||
endpoint: 0.0.0.0:55678
|
||||
otlp/spanmetrics:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: "localhost:12345"
|
||||
endpoint: localhost:12345
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
jaeger:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:14250
|
||||
thrift_http:
|
||||
endpoint: 0.0.0.0:14268
|
||||
# thrift_compact:
|
||||
# endpoint: 0.0.0.0:6831
|
||||
# thrift_binary:
|
||||
# endpoint: 0.0.0.0:6832
|
||||
hostmetrics:
|
||||
collection_interval: 60s
|
||||
scrapers:
|
||||
cpu:
|
||||
load:
|
||||
memory:
|
||||
disk:
|
||||
filesystem:
|
||||
network:
|
||||
cpu: {}
|
||||
load: {}
|
||||
memory: {}
|
||||
disk: {}
|
||||
filesystem: {}
|
||||
network: {}
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
@ -49,9 +60,20 @@ processors:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
resourcedetection:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
override: false
|
||||
|
||||
extensions:
|
||||
health_check: {}
|
||||
zpages: {}
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
@ -60,18 +82,30 @@ exporters:
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
prometheus:
|
||||
endpoint: "0.0.0.0:8889"
|
||||
endpoint: 0.0.0.0:8889
|
||||
# logging: {}
|
||||
|
||||
service:
|
||||
extensions: [health_check, zpages]
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- zpages
|
||||
- pprof
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [jaeger, otlp]
|
||||
processors: [signozspanmetrics/prometheus, batch]
|
||||
exporters: [clickhousetraces]
|
||||
metrics:
|
||||
receivers: [otlp, hostmetrics]
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/hostmetrics:
|
||||
receivers: [hostmetrics]
|
||||
processors: [resourcedetection, batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/spanmetrics:
|
||||
receivers: [otlp/spanmetrics]
|
||||
exporters: [prometheus]
|
||||
|
@ -3,15 +3,28 @@ receivers:
|
||||
protocols:
|
||||
grpc:
|
||||
http:
|
||||
|
||||
# Data sources: metrics
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: "otel-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets: ["otel-collector:8889"]
|
||||
- targets:
|
||||
- otel-collector:8888
|
||||
# otel-collector-metrics internal metrics
|
||||
- job_name: "otel-collector-metrics"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
# SigNoz span metrics
|
||||
- job_name: "signozspanmetrics-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- otel-collector:8889
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
@ -32,17 +45,29 @@ processors:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
|
||||
extensions:
|
||||
health_check: {}
|
||||
zpages: {}
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
exporters:
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
|
||||
service:
|
||||
extensions: [health_check, zpages]
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- zpages
|
||||
- pprof
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp, prometheus]
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
|
@ -1,123 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<clickhouse>
|
||||
<!-- See also the files in users.d directory where the settings can be overridden. -->
|
||||
|
||||
<!-- Profiles of settings. -->
|
||||
<profiles>
|
||||
<!-- Default settings. -->
|
||||
<default>
|
||||
<!-- Maximum memory usage for processing single query, in bytes. -->
|
||||
<max_memory_usage>10000000000</max_memory_usage>
|
||||
|
||||
<!-- How to choose between replicas during distributed query processing.
|
||||
random - choose random replica from set of replicas with minimum number of errors
|
||||
nearest_hostname - from set of replicas with minimum number of errors, choose replica
|
||||
with minimum number of different symbols between replica's hostname and local hostname
|
||||
(Hamming distance).
|
||||
in_order - first live replica is chosen in specified order.
|
||||
first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
|
||||
-->
|
||||
<load_balancing>random</load_balancing>
|
||||
</default>
|
||||
|
||||
<!-- Profile that allows only read queries. -->
|
||||
<readonly>
|
||||
<readonly>1</readonly>
|
||||
</readonly>
|
||||
</profiles>
|
||||
|
||||
<!-- Users and ACL. -->
|
||||
<users>
|
||||
<!-- If user name was not specified, 'default' user is used. -->
|
||||
<default>
|
||||
<!-- See also the files in users.d directory where the password can be overridden.
|
||||
|
||||
Password could be specified in plaintext or in SHA256 (in hex format).
|
||||
|
||||
If you want to specify password in plaintext (not recommended), place it in 'password' element.
|
||||
Example: <password>qwerty</password>.
|
||||
Password could be empty.
|
||||
|
||||
If you want to specify SHA256, place it in 'password_sha256_hex' element.
|
||||
Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
|
||||
Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).
|
||||
|
||||
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
|
||||
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
|
||||
|
||||
If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
|
||||
place its name in 'server' element inside 'ldap' element.
|
||||
Example: <ldap><server>my_ldap_server</server></ldap>
|
||||
|
||||
If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),
|
||||
place 'kerberos' element instead of 'password' (and similar) elements.
|
||||
The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.
|
||||
You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests
|
||||
whose initiator's realm matches it.
|
||||
Example: <kerberos />
|
||||
Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>
|
||||
|
||||
How to generate decent password:
|
||||
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
|
||||
In first line will be password and in second - corresponding SHA256.
|
||||
|
||||
How to generate double SHA1:
|
||||
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'
|
||||
In first line will be password and in second - corresponding double SHA1.
|
||||
-->
|
||||
<password></password>
|
||||
|
||||
<!-- List of networks with open access.
|
||||
|
||||
To open access from everywhere, specify:
|
||||
<ip>::/0</ip>
|
||||
|
||||
To open access only from localhost, specify:
|
||||
<ip>::1</ip>
|
||||
<ip>127.0.0.1</ip>
|
||||
|
||||
Each element of list has one of the following forms:
|
||||
<ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
|
||||
2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
|
||||
<host> Hostname. Example: server01.clickhouse.com.
|
||||
To check access, DNS query is performed, and all received addresses compared to peer address.
|
||||
<host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.clickhouse\.com$
|
||||
To check access, DNS PTR query is performed for peer address and then regexp is applied.
|
||||
Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
|
||||
Strongly recommended that regexp is ends with $
|
||||
All results of DNS requests are cached till server restart.
|
||||
-->
|
||||
<networks>
|
||||
<ip>::/0</ip>
|
||||
</networks>
|
||||
|
||||
<!-- Settings profile for user. -->
|
||||
<profile>default</profile>
|
||||
|
||||
<!-- Quota for user. -->
|
||||
<quota>default</quota>
|
||||
|
||||
<!-- User can create other users and grant rights to them. -->
|
||||
<!-- <access_management>1</access_management> -->
|
||||
</default>
|
||||
</users>
|
||||
|
||||
<!-- Quotas. -->
|
||||
<quotas>
|
||||
<!-- Name of quota. -->
|
||||
<default>
|
||||
<!-- Limits for time interval. You could specify many intervals with different limits. -->
|
||||
<interval>
|
||||
<!-- Length of interval. -->
|
||||
<duration>3600</duration>
|
||||
|
||||
<!-- No limits. Just calculate resource usage for time interval. -->
|
||||
<queries>0</queries>
|
||||
<errors>0</errors>
|
||||
<result_rows>0</result_rows>
|
||||
<read_rows>0</read_rows>
|
||||
<execution_time>0</execution_time>
|
||||
</interval>
|
||||
</default>
|
||||
</quotas>
|
||||
</clickhouse>
|
@ -13,8 +13,9 @@
|
||||
"jest:coverage": "jest --coverage",
|
||||
"jest:watch": "jest --watch",
|
||||
"postinstall": "is-ci || yarn husky:configure",
|
||||
"playwright": "playwright test --config=./playwright.config.ts",
|
||||
"playwright": "NODE_ENV=testing playwright test --config=./playwright.config.ts",
|
||||
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
|
||||
"playwright:codegen:local":"playwright codegen http://localhost:3301",
|
||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||
"commitlint": "commitlint --edit $1"
|
||||
},
|
||||
@ -43,6 +44,7 @@
|
||||
"babel-preset-react-app": "^10.0.0",
|
||||
"chart.js": "^3.4.0",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"chartjs-plugin-annotation": "^1.4.0",
|
||||
"color": "^4.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "4.3.0",
|
||||
@ -81,6 +83,7 @@
|
||||
"style-loader": "1.3.0",
|
||||
"styled-components": "^5.2.1",
|
||||
"terser-webpack-plugin": "^5.2.5",
|
||||
"timestamp-nano": "^1.0.0",
|
||||
"ts-node": "^10.2.1",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typescript": "^4.0.5",
|
||||
|
@ -16,6 +16,8 @@ const config: PlaywrightTestConfig = {
|
||||
updateSnapshots: 'all',
|
||||
fullyParallel: false,
|
||||
quiet: true,
|
||||
testMatch: ['**/*.spec.ts'],
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
85
frontend/public/locales/en-GB/alerts.json
Normal file
85
frontend/public/locales/en-GB/alerts.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"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",
|
||||
"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",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info",
|
||||
"user_guide_headline": "Steps to create an Alert",
|
||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts"
|
||||
}
|
85
frontend/public/locales/en-GB/rules.json
Normal file
85
frontend/public/locales/en-GB/rules.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"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",
|
||||
"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",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info",
|
||||
"user_guide_headline": "Steps to create an Alert",
|
||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts"
|
||||
}
|
85
frontend/public/locales/en/alerts.json
Normal file
85
frontend/public/locales/en/alerts.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"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",
|
||||
"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",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info",
|
||||
"user_guide_headline": "Steps to create an Alert",
|
||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts"
|
||||
}
|
85
frontend/public/locales/en/rules.json
Normal file
85
frontend/public/locales/en/rules.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"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",
|
||||
"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",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info",
|
||||
"user_guide_headline": "Steps to create an Alert",
|
||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts"
|
||||
}
|
@ -9,7 +9,7 @@ const create = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/rules', {
|
||||
data: props.query,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -14,7 +14,7 @@ const get = async (
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
|
@ -2,14 +2,14 @@ import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/put';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
const put = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/rules/${props.id}`, {
|
||||
data: props.data,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
|
17
frontend/src/api/alerts/save.ts
Normal file
17
frontend/src/api/alerts/save.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
import create from './create';
|
||||
import put from './put';
|
||||
|
||||
const save = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
if (props.id && props.id > 0) {
|
||||
return put({ ...props });
|
||||
}
|
||||
|
||||
return create({ ...props });
|
||||
};
|
||||
|
||||
export default save;
|
@ -10,9 +10,8 @@ const getAll = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/errors?${createQueryParams({
|
||||
start: props.start.toString(),
|
||||
end: props.end.toString(),
|
||||
`/listErrors?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
|
@ -10,11 +10,8 @@ const getByErrorType = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/errorWithType?${createQueryParams({
|
||||
start: props.start.toString(),
|
||||
end: props.end.toString(),
|
||||
serviceName: props.serviceName,
|
||||
errorType: props.errorType,
|
||||
`/errorFromGroupID?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
|
@ -3,17 +3,15 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getById';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getByErrorId';
|
||||
|
||||
const getById = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/errorWithId?${createQueryParams({
|
||||
start: props.start.toString(),
|
||||
end: props.end.toString(),
|
||||
errorId: props.errorId,
|
||||
`/errorFromErrorID?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
|
29
frontend/src/api/errors/getErrorCounts.ts
Normal file
29
frontend/src/api/errors/getErrorCounts.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getErrorCounts';
|
||||
|
||||
const getErrorCounts = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/countErrors?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.message,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getErrorCounts;
|
29
frontend/src/api/errors/getNextPrevId.ts
Normal file
29
frontend/src/api/errors/getNextPrevId.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getNextPrevId';
|
||||
|
||||
const getErrorCounts = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/nextPrevErrorIDs?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.message,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getErrorCounts;
|
@ -1,14 +1,15 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { getVersion } from 'constants/api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/user/getVersion';
|
||||
|
||||
const getVersion = async (): Promise<
|
||||
const getVersionApi = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get(`/version`);
|
||||
const response = await axios.get(`/${getVersion}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
@ -21,4 +22,4 @@ const getVersion = async (): Promise<
|
||||
}
|
||||
};
|
||||
|
||||
export default getVersion;
|
||||
export default getVersionApi;
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
Tooltip,
|
||||
} from 'chart.js';
|
||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -50,6 +51,7 @@ Chart.register(
|
||||
SubTitle,
|
||||
BarController,
|
||||
BarElement,
|
||||
annotationPlugin,
|
||||
);
|
||||
|
||||
function Graph({
|
||||
@ -62,6 +64,7 @@ function Graph({
|
||||
name,
|
||||
yAxisUnit = 'short',
|
||||
forceReRender,
|
||||
staticLine,
|
||||
}: GraphProps): JSX.Element {
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
@ -99,6 +102,30 @@ function Graph({
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
annotation: staticLine
|
||||
? {
|
||||
annotations: [
|
||||
{
|
||||
type: 'line',
|
||||
yMin: staticLine.yMin,
|
||||
yMax: staticLine.yMax,
|
||||
borderColor: staticLine.borderColor,
|
||||
borderWidth: staticLine.borderWidth,
|
||||
label: {
|
||||
content: staticLine.lineText,
|
||||
enabled: true,
|
||||
font: {
|
||||
size: 10,
|
||||
},
|
||||
borderWidth: 0,
|
||||
position: 'start',
|
||||
backgroundColor: 'transparent',
|
||||
color: staticLine.textColor,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
title: {
|
||||
display: title !== undefined,
|
||||
text: title,
|
||||
@ -180,6 +207,7 @@ function Graph({
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const chartHasData = hasData(data);
|
||||
const chartPlugins = [];
|
||||
|
||||
@ -205,6 +233,7 @@ function Graph({
|
||||
name,
|
||||
yAxisUnit,
|
||||
onClickHandler,
|
||||
staticLine,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -229,6 +258,16 @@ interface GraphProps {
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
forceReRender?: boolean | null | number;
|
||||
staticLine?: StaticLineProps | undefined;
|
||||
}
|
||||
|
||||
export interface StaticLineProps {
|
||||
yMin: number | undefined;
|
||||
yMax: number | undefined;
|
||||
borderColor: string;
|
||||
borderWidth: number;
|
||||
lineText: string;
|
||||
textColor: string;
|
||||
}
|
||||
|
||||
export type GraphOnClickHandler = (
|
||||
@ -245,5 +284,6 @@ Graph.defaultProps = {
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
forceReRender: undefined,
|
||||
staticLine: undefined,
|
||||
};
|
||||
export default Graph;
|
||||
|
3
frontend/src/constants/api.ts
Normal file
3
frontend/src/constants/api.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const getVersion = 'version';
|
||||
|
||||
export { getVersion };
|
@ -1,31 +1,85 @@
|
||||
import { notification, Table, Tooltip, Typography } from 'antd';
|
||||
import { notification, Table, TableProps, Tooltip, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import getAll from 'api/errors/getAll';
|
||||
import getErrorCounts from 'api/errors/getErrorCounts';
|
||||
import ROUTES from 'constants/routes';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useEffect } from 'react';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Exception } from 'types/api/errors/getAll';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Exception, PayloadProps } from 'types/api/errors/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
getDefaultOrder,
|
||||
getNanoSeconds,
|
||||
getOffSet,
|
||||
getOrder,
|
||||
getOrderParams,
|
||||
getUpdatePageSize,
|
||||
urlKey,
|
||||
} from './utils';
|
||||
|
||||
function AllErrors(): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
const { maxTime, minTime, loading } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { search, pathname } = useLocation();
|
||||
const params = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
const { isLoading, data } = useQuery(['getAllError', [maxTime, minTime]], {
|
||||
queryFn: () =>
|
||||
getAll({
|
||||
end: maxTime,
|
||||
start: minTime,
|
||||
}),
|
||||
});
|
||||
const updatedOrder = getOrder(params.get(urlKey.order));
|
||||
const getUpdatedOffset = getOffSet(params.get(urlKey.offset));
|
||||
const getUpdatedParams = getOrderParams(params.get(urlKey.orderParam));
|
||||
const getUpdatedPageSize = getUpdatePageSize(params.get(urlKey.pageSize));
|
||||
|
||||
const updatedPath = useMemo(
|
||||
() =>
|
||||
`${pathname}?${createQueryParams({
|
||||
order: updatedOrder,
|
||||
offset: getUpdatedOffset,
|
||||
orderParam: getUpdatedParams,
|
||||
pageSize: getUpdatedPageSize,
|
||||
})}`,
|
||||
[
|
||||
pathname,
|
||||
updatedOrder,
|
||||
getUpdatedOffset,
|
||||
getUpdatedParams,
|
||||
getUpdatedPageSize,
|
||||
],
|
||||
);
|
||||
|
||||
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
||||
{
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime],
|
||||
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
||||
getAll({
|
||||
end: maxTime,
|
||||
start: minTime,
|
||||
order: updatedOrder,
|
||||
limit: getUpdatedPageSize,
|
||||
offset: getUpdatedOffset,
|
||||
orderParam: getUpdatedParams,
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
{
|
||||
queryKey: ['getErrorCounts', maxTime, minTime],
|
||||
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
||||
getErrorCounts({
|
||||
end: maxTime,
|
||||
start: minTime,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.error) {
|
||||
@ -35,11 +89,9 @@ function AllErrors(): JSX.Element {
|
||||
}
|
||||
}, [data?.error, data?.payload, t]);
|
||||
|
||||
const getDateValue = (value: string): JSX.Element => {
|
||||
return (
|
||||
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
|
||||
);
|
||||
};
|
||||
const getDateValue = (value: string): JSX.Element => (
|
||||
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
|
||||
);
|
||||
|
||||
const columns: ColumnsType<Exception> = [
|
||||
{
|
||||
@ -49,14 +101,20 @@ function AllErrors(): JSX.Element {
|
||||
render: (value, record): JSX.Element => (
|
||||
<Tooltip overlay={(): JSX.Element => value}>
|
||||
<Link
|
||||
to={`${ROUTES.ERROR_DETAIL}?serviceName=${record.serviceName}&errorType=${record.exceptionType}`}
|
||||
to={`${ROUTES.ERROR_DETAIL}?groupId=${
|
||||
record.groupID
|
||||
}×tamp=${getNanoSeconds(record.lastSeen)}`}
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
),
|
||||
sorter: (a, b): number =>
|
||||
a.exceptionType.charCodeAt(0) - b.exceptionType.charCodeAt(0),
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'exceptionType',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Error Message',
|
||||
@ -78,39 +136,86 @@ function AllErrors(): JSX.Element {
|
||||
title: 'Count',
|
||||
dataIndex: 'exceptionCount',
|
||||
key: 'exceptionCount',
|
||||
sorter: (a, b): number => a.exceptionCount - b.exceptionCount,
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'exceptionCount',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Last Seen',
|
||||
dataIndex: 'lastSeen',
|
||||
key: 'lastSeen',
|
||||
render: getDateValue,
|
||||
sorter: (a, b): number =>
|
||||
dayjs(b.lastSeen).isBefore(dayjs(a.lastSeen)) === true ? 1 : 0,
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'lastSeen',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'First Seen',
|
||||
dataIndex: 'firstSeen',
|
||||
key: 'firstSeen',
|
||||
render: getDateValue,
|
||||
sorter: (a, b): number =>
|
||||
dayjs(b.firstSeen).isBefore(dayjs(a.firstSeen)) === true ? 1 : 0,
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'firstSeen',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
dataIndex: 'serviceName',
|
||||
key: 'serviceName',
|
||||
sorter: (a, b): number =>
|
||||
a.serviceName.charCodeAt(0) - b.serviceName.charCodeAt(0),
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'serviceName',
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onChangeHandler: TableProps<Exception>['onChange'] = (
|
||||
paginations,
|
||||
_,
|
||||
sorter,
|
||||
) => {
|
||||
if (!Array.isArray(sorter)) {
|
||||
const { pageSize = 0, current = 0 } = paginations;
|
||||
const { columnKey = '', order } = sorter;
|
||||
const updatedOrder = order === 'ascend' ? 'ascending' : 'descending';
|
||||
|
||||
history.replace(
|
||||
`${pathname}?${createQueryParams({
|
||||
order: updatedOrder,
|
||||
offset: (current - 1) * pageSize,
|
||||
orderParam: columnKey,
|
||||
pageSize,
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
tableLayout="fixed"
|
||||
dataSource={data?.payload as Exception[]}
|
||||
columns={columns}
|
||||
loading={isLoading || false}
|
||||
rowKey="firstSeen"
|
||||
loading={isLoading || false || errorCountResponse.status === 'loading'}
|
||||
pagination={{
|
||||
pageSize: getUpdatedPageSize,
|
||||
responsive: true,
|
||||
current: getUpdatedOffset / 10 + 1,
|
||||
position: ['bottomLeft'],
|
||||
total: errorCountResponse.data?.payload || 0,
|
||||
}}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
109
frontend/src/container/AllError/utils.test.ts
Normal file
109
frontend/src/container/AllError/utils.test.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { Order, OrderBy } from 'types/api/errors/getAll';
|
||||
|
||||
import {
|
||||
getDefaultOrder,
|
||||
getLimit,
|
||||
getOffSet,
|
||||
getOrder,
|
||||
getOrderParams,
|
||||
getUpdatePageSize,
|
||||
isOrder,
|
||||
isOrderParams,
|
||||
} from './utils';
|
||||
|
||||
describe('Error utils', () => {
|
||||
test('Valid OrderBy Params', () => {
|
||||
expect(isOrderParams('serviceName')).toBe(true);
|
||||
expect(isOrderParams('exceptionCount')).toBe(true);
|
||||
expect(isOrderParams('lastSeen')).toBe(true);
|
||||
expect(isOrderParams('firstSeen')).toBe(true);
|
||||
expect(isOrderParams('exceptionType')).toBe(true);
|
||||
});
|
||||
|
||||
test('Invalid OrderBy Params', () => {
|
||||
expect(isOrderParams('invalid')).toBe(false);
|
||||
expect(isOrderParams(null)).toBe(false);
|
||||
expect(isOrderParams('')).toBe(false);
|
||||
});
|
||||
|
||||
test('Valid Order', () => {
|
||||
expect(isOrder('ascending')).toBe(true);
|
||||
expect(isOrder('descending')).toBe(true);
|
||||
});
|
||||
|
||||
test('Invalid Order', () => {
|
||||
expect(isOrder('invalid')).toBe(false);
|
||||
expect(isOrder(null)).toBe(false);
|
||||
expect(isOrder('')).toBe(false);
|
||||
});
|
||||
|
||||
test('Default Order', () => {
|
||||
const OrderBy: OrderBy[] = [
|
||||
'exceptionCount',
|
||||
'exceptionType',
|
||||
'firstSeen',
|
||||
'lastSeen',
|
||||
'serviceName',
|
||||
];
|
||||
|
||||
const order: Order[] = ['ascending', 'descending'];
|
||||
|
||||
const ascOrd = order[0];
|
||||
const desOrd = order[1];
|
||||
|
||||
OrderBy.forEach((order) => {
|
||||
expect(getDefaultOrder(order, ascOrd, order)).toBe('ascend');
|
||||
expect(getDefaultOrder(order, desOrd, order)).toBe('descend');
|
||||
});
|
||||
});
|
||||
|
||||
test('Limit', () => {
|
||||
expect(getLimit(null)).toBe(10);
|
||||
expect(getLimit('')).toBe(10);
|
||||
expect(getLimit('0')).toBe(0);
|
||||
expect(getLimit('1')).toBe(1);
|
||||
expect(getLimit('10')).toBe(10);
|
||||
expect(getLimit('11')).toBe(11);
|
||||
expect(getLimit('100')).toBe(100);
|
||||
expect(getLimit('101')).toBe(101);
|
||||
});
|
||||
|
||||
test('Update Page Size', () => {
|
||||
expect(getUpdatePageSize(null)).toBe(10);
|
||||
expect(getUpdatePageSize('')).toBe(10);
|
||||
expect(getUpdatePageSize('0')).toBe(0);
|
||||
expect(getUpdatePageSize('1')).toBe(1);
|
||||
expect(getUpdatePageSize('10')).toBe(10);
|
||||
expect(getUpdatePageSize('11')).toBe(11);
|
||||
expect(getUpdatePageSize('100')).toBe(100);
|
||||
expect(getUpdatePageSize('101')).toBe(101);
|
||||
});
|
||||
|
||||
test('Order Params', () => {
|
||||
expect(getOrderParams(null)).toBe('serviceName');
|
||||
expect(getOrderParams('')).toBe('serviceName');
|
||||
expect(getOrderParams('serviceName')).toBe('serviceName');
|
||||
expect(getOrderParams('exceptionCount')).toBe('exceptionCount');
|
||||
expect(getOrderParams('lastSeen')).toBe('lastSeen');
|
||||
expect(getOrderParams('firstSeen')).toBe('firstSeen');
|
||||
expect(getOrderParams('exceptionType')).toBe('exceptionType');
|
||||
});
|
||||
|
||||
test('OffSet', () => {
|
||||
expect(getOffSet(null)).toBe(0);
|
||||
expect(getOffSet('')).toBe(0);
|
||||
expect(getOffSet('0')).toBe(0);
|
||||
expect(getOffSet('1')).toBe(1);
|
||||
expect(getOffSet('10')).toBe(10);
|
||||
expect(getOffSet('11')).toBe(11);
|
||||
expect(getOffSet('100')).toBe(100);
|
||||
expect(getOffSet('101')).toBe(101);
|
||||
});
|
||||
|
||||
test('Order', () => {
|
||||
expect(getOrder(null)).toBe('ascending');
|
||||
expect(getOrder('')).toBe('ascending');
|
||||
expect(getOrder('ascending')).toBe('ascending');
|
||||
expect(getOrder('descending')).toBe('descending');
|
||||
});
|
||||
});
|
89
frontend/src/container/AllError/utils.ts
Normal file
89
frontend/src/container/AllError/utils.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { SortOrder } from 'antd/lib/table/interface';
|
||||
import Timestamp from 'timestamp-nano';
|
||||
import { Order, OrderBy } from 'types/api/errors/getAll';
|
||||
|
||||
export const isOrder = (order: string | null): order is Order =>
|
||||
!!(order === 'ascending' || order === 'descending');
|
||||
|
||||
export const urlKey = {
|
||||
order: 'order',
|
||||
offset: 'offset',
|
||||
orderParam: 'orderParam',
|
||||
pageSize: 'pageSize',
|
||||
};
|
||||
|
||||
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
|
||||
return !!(
|
||||
orderBy === 'serviceName' ||
|
||||
orderBy === 'exceptionCount' ||
|
||||
orderBy === 'lastSeen' ||
|
||||
orderBy === 'firstSeen' ||
|
||||
orderBy === 'exceptionType'
|
||||
);
|
||||
};
|
||||
|
||||
export const getOrder = (order: string | null): Order => {
|
||||
if (isOrder(order)) {
|
||||
return order;
|
||||
}
|
||||
return 'ascending';
|
||||
};
|
||||
|
||||
export const getLimit = (limit: string | null): number => {
|
||||
if (limit) {
|
||||
return parseInt(limit, 10);
|
||||
}
|
||||
return 10;
|
||||
};
|
||||
|
||||
export const getOffSet = (offset: string | null): number => {
|
||||
if (offset && typeof offset === 'string') {
|
||||
return parseInt(offset, 10);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const getOrderParams = (order: string | null): OrderBy => {
|
||||
if (isOrderParams(order)) {
|
||||
return order;
|
||||
}
|
||||
return 'serviceName';
|
||||
};
|
||||
|
||||
export const getDefaultOrder = (
|
||||
orderBy: OrderBy,
|
||||
order: Order,
|
||||
data: OrderBy,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): SortOrder | undefined => {
|
||||
if (orderBy === 'exceptionType' && data === 'exceptionType') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
if (orderBy === 'serviceName' && data === 'serviceName') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
if (orderBy === 'exceptionCount' && data === 'exceptionCount') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
if (orderBy === 'lastSeen' && data === 'lastSeen') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
if (orderBy === 'firstSeen' && data === 'firstSeen') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getNanoSeconds = (date: string): string => {
|
||||
return (
|
||||
Math.floor(new Date(date).getTime() / 1e3).toString() +
|
||||
Timestamp.fromString(date).getNano().toString()
|
||||
);
|
||||
};
|
||||
|
||||
export const getUpdatePageSize = (pageSize: string | null): number => {
|
||||
if (pageSize) {
|
||||
return parseInt(pageSize, 10);
|
||||
}
|
||||
return 10;
|
||||
};
|
22
frontend/src/container/CreateAlertRule/index.tsx
Normal file
22
frontend/src/container/CreateAlertRule/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Form } from 'antd';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import React from 'react';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
function CreateRules({ initialValue }: CreateRulesProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
return (
|
||||
<FormAlertRules
|
||||
formInstance={formInstance}
|
||||
initialValue={initialValue}
|
||||
ruleId={0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface CreateRulesProps {
|
||||
initialValue: AlertDef;
|
||||
}
|
||||
|
||||
export default CreateRules;
|
@ -1,102 +1,23 @@
|
||||
import { SaveFilled } from '@ant-design/icons';
|
||||
import { Button, notification } from 'antd';
|
||||
import put from 'api/alerts/put';
|
||||
import Editor from 'components/Editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { PayloadProps } from 'types/api/alerts/get';
|
||||
import { PayloadProps as PutPayloadProps } from 'types/api/alerts/put';
|
||||
import { Form } from 'antd';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import React from 'react';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { ButtonContainer } from './styles';
|
||||
|
||||
function EditRules({ initialData, ruleId }: EditRulesProps): JSX.Element {
|
||||
const [value, setEditorValue] = useState<string>(initialData);
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
const [editButtonState, setEditButtonState] = useState<State<PutPayloadProps>>(
|
||||
{
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
success: false,
|
||||
payload: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const onClickHandler = useCallback(async () => {
|
||||
try {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
const response = await put({
|
||||
data: value,
|
||||
id: parseInt(ruleId, 10),
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Congrats. The alert was Edited correctly.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
}, 2000);
|
||||
} else {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
error: true,
|
||||
}));
|
||||
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
response.error ||
|
||||
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
|
||||
});
|
||||
}
|
||||
}, [value, ruleId, notifications]);
|
||||
function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Editor onChange={(value): void => setEditorValue(value)} value={value} />
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={editButtonState.loading || false}
|
||||
disabled={editButtonState.loading || false}
|
||||
icon={<SaveFilled />}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
<FormAlertRules
|
||||
formInstance={formInstance}
|
||||
initialValue={initialValue}
|
||||
ruleId={ruleId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditRulesProps {
|
||||
initialData: PayloadProps['data'];
|
||||
ruleId: string;
|
||||
initialValue: AlertDef;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
@ -1,25 +1,49 @@
|
||||
import { Button, Divider, notification, Space, Table, Typography } from 'antd';
|
||||
import getNextPrevId from 'api/errors/getNextPrevId';
|
||||
import Editor from 'components/Editor';
|
||||
import { getNanoSeconds } from 'container/AllError/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import history from 'lib/history';
|
||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
|
||||
import { PayloadProps } from 'types/api/errors/getById';
|
||||
|
||||
import { DashedContainer, EditorContainer, EventContainer } from './styles';
|
||||
|
||||
function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
const { idPayload } = props;
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const { t } = useTranslation(['errorDetails', 'common']);
|
||||
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const queryErrorId = params.get('errorId');
|
||||
const serviceName = params.get('serviceName');
|
||||
const errorType = params.get('errorType');
|
||||
|
||||
const params = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const errorId = params.get(urlKey.errorId);
|
||||
const serviceName = params.get(urlKey.serviceName);
|
||||
const errorType = params.get(urlKey.exceptionType);
|
||||
const timestamp = params.get(urlKey.timestamp);
|
||||
|
||||
const { data: nextPrevData, status: nextPrevStatus } = useQuery(
|
||||
[
|
||||
idPayload.errorId,
|
||||
idPayload.groupID,
|
||||
idPayload.timestamp,
|
||||
errorId,
|
||||
serviceName,
|
||||
errorType,
|
||||
timestamp,
|
||||
],
|
||||
{
|
||||
queryFn: () =>
|
||||
getNextPrevId({
|
||||
errorID: errorId || idPayload.errorId,
|
||||
groupID: idPayload.groupID,
|
||||
timestamp: timestamp || getNanoSeconds(idPayload.timestamp),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const errorDetail = idPayload;
|
||||
|
||||
@ -48,34 +72,32 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
'errorId',
|
||||
'timestamp',
|
||||
'exceptionMessage',
|
||||
'newerErrorId',
|
||||
'olderErrorId',
|
||||
'exceptionEscaped',
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const onClickErrorIdHandler = async (id: string): Promise<void> => {
|
||||
const onClickErrorIdHandler = async (
|
||||
id: string,
|
||||
timestamp: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
if (id.length === 0) {
|
||||
notification.error({
|
||||
message: 'Error Id cannot be empty',
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
||||
history.push(
|
||||
`${history.location.pathname}?errorId=${id}&serviceName=${serviceName}&errorType=${errorType}`,
|
||||
history.replace(
|
||||
`${history.location.pathname}?&groupId=${
|
||||
idPayload.groupID
|
||||
}×tamp=${getNanoSeconds(timestamp)}&errorId=${id}`,
|
||||
);
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: t('something_went_wrong'),
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -106,25 +128,25 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
<div>
|
||||
<Space align="end" direction="horizontal">
|
||||
<Button
|
||||
loading={isLoading}
|
||||
disabled={
|
||||
errorDetail.olderErrorId.length === 0 ||
|
||||
queryErrorId === errorDetail.olderErrorId
|
||||
}
|
||||
loading={nextPrevStatus === 'loading'}
|
||||
disabled={nextPrevData?.payload?.prevErrorID.length === 0}
|
||||
onClick={(): Promise<void> =>
|
||||
onClickErrorIdHandler(errorDetail.olderErrorId)
|
||||
onClickErrorIdHandler(
|
||||
nextPrevData?.payload?.prevErrorID || '',
|
||||
nextPrevData?.payload?.prevTimestamp || '',
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('older')}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
disabled={
|
||||
errorDetail.newerErrorId.length === 0 ||
|
||||
queryErrorId === errorDetail.newerErrorId
|
||||
}
|
||||
loading={nextPrevStatus === 'loading'}
|
||||
disabled={nextPrevData?.payload?.nextErrorID.length === 0}
|
||||
onClick={(): Promise<void> =>
|
||||
onClickErrorIdHandler(errorDetail.newerErrorId)
|
||||
onClickErrorIdHandler(
|
||||
nextPrevData?.payload?.nextErrorID || '',
|
||||
nextPrevData?.payload?.nextTimestamp || '',
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('newer')}
|
||||
@ -153,7 +175,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface ErrorDetailsProps {
|
||||
idPayload: PayloadProps;
|
||||
idPayload: GetByErrorTypeAndServicePayload;
|
||||
}
|
||||
|
||||
export default ErrorDetails;
|
||||
|
101
frontend/src/container/FormAlertRules/BasicInfo.tsx
Normal file
101
frontend/src/container/FormAlertRules/BasicInfo.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { Select } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
|
||||
import LabelSelect from './labels';
|
||||
import {
|
||||
FormContainer,
|
||||
InputSmall,
|
||||
SeveritySelect,
|
||||
StepHeading,
|
||||
TextareaMedium,
|
||||
} from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface BasicInfoProps {
|
||||
alertDef: AlertDef;
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
}
|
||||
|
||||
function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
||||
<FormContainer>
|
||||
<FormItem
|
||||
label={t('field_severity')}
|
||||
labelAlign="left"
|
||||
name={['labels', 'severity']}
|
||||
>
|
||||
<SeveritySelect
|
||||
defaultValue="critical"
|
||||
onChange={(value: unknown | string): void => {
|
||||
const s = (value as string) || 'critical';
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
labels: {
|
||||
...alertDef.labels,
|
||||
severity: s,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Option value="critical">{t('option_critical')}</Option>
|
||||
<Option value="error">{t('option_error')}</Option>
|
||||
<Option value="warning">{t('option_warning')}</Option>
|
||||
<Option value="info">{t('option_info')}</Option>
|
||||
</SeveritySelect>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
|
||||
<InputSmall
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
alert: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={t('field_alert_desc')}
|
||||
labelAlign="left"
|
||||
name={['annotations', 'description']}
|
||||
>
|
||||
<TextareaMedium
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
annotations: {
|
||||
...alertDef.annotations,
|
||||
description: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={t('field_labels')}>
|
||||
<LabelSelect
|
||||
onSetLabels={(l: Labels): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
labels: {
|
||||
...l,
|
||||
},
|
||||
});
|
||||
}}
|
||||
initialValues={alertDef.labels}
|
||||
/>
|
||||
</FormItem>
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default BasicInfo;
|
119
frontend/src/container/FormAlertRules/ChartPreview/index.tsx
Normal file
119
frontend/src/container/FormAlertRules/ChartPreview/index.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { StaticLineProps } from 'components/Graph';
|
||||
import GridGraphComponent from 'container/GridGraphComponent';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||
import { Query } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||
|
||||
export interface ChartPreviewProps {
|
||||
name: string;
|
||||
query: Query | undefined;
|
||||
graphType?: GRAPH_TYPES;
|
||||
selectedTime?: timePreferenceType;
|
||||
selectedInterval?: Time;
|
||||
headline?: JSX.Element;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
function ChartPreview({
|
||||
name,
|
||||
query,
|
||||
graphType = 'TIME_SERIES',
|
||||
selectedTime = 'GLOBAL_TIME',
|
||||
selectedInterval = '5min',
|
||||
headline,
|
||||
threshold,
|
||||
}: ChartPreviewProps): JSX.Element | null {
|
||||
const { t } = useTranslation('alerts');
|
||||
const staticLine: StaticLineProps | undefined =
|
||||
threshold && threshold > 0
|
||||
? {
|
||||
yMin: threshold,
|
||||
yMax: threshold,
|
||||
borderColor: '#f14',
|
||||
borderWidth: 1,
|
||||
lineText: `${t('preview_chart_threshold_label')} (y=${threshold})`,
|
||||
textColor: '#f14',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const queryKey = JSON.stringify(query);
|
||||
const queryResponse = useQuery({
|
||||
queryKey: ['chartPreview', queryKey, selectedInterval],
|
||||
queryFn: () =>
|
||||
GetMetricQueryRange({
|
||||
query: query || {
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: {
|
||||
formulas: [],
|
||||
queryBuilder: [],
|
||||
},
|
||||
clickHouse: [],
|
||||
},
|
||||
globalSelectedInterval: selectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
}),
|
||||
enabled:
|
||||
query != null &&
|
||||
(query.queryType !== EQueryType.PROM ||
|
||||
(query.promQL?.length > 0 && query.promQL[0].query !== '')),
|
||||
});
|
||||
|
||||
const chartDataSet = queryResponse.isError
|
||||
? null
|
||||
: getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result
|
||||
? queryResponse?.data?.payload?.data?.result
|
||||
: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<ChartContainer>
|
||||
{headline}
|
||||
{(queryResponse?.data?.error || queryResponse?.isError) && (
|
||||
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
||||
<InfoCircleOutlined />{' '}
|
||||
{queryResponse?.data?.error ||
|
||||
queryResponse?.error ||
|
||||
t('preview_chart_unexpected_error')}
|
||||
</FailedMessageContainer>
|
||||
)}
|
||||
|
||||
{chartDataSet && !queryResponse.isError && (
|
||||
<GridGraphComponent
|
||||
title={name}
|
||||
data={chartDataSet}
|
||||
isStacked
|
||||
GRAPH_TYPES={graphType || 'TIME_SERIES'}
|
||||
name={name || 'Chart Preview'}
|
||||
staticLine={staticLine}
|
||||
/>
|
||||
)}
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
ChartPreview.defaultProps = {
|
||||
graphType: 'TIME_SERIES',
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
selectedInterval: '5min',
|
||||
headline: undefined,
|
||||
threshold: 0,
|
||||
};
|
||||
|
||||
export default ChartPreview;
|
28
frontend/src/container/FormAlertRules/ChartPreview/styles.ts
Normal file
28
frontend/src/container/FormAlertRules/ChartPreview/styles.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Card, Tooltip } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const NotFoundContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 55vh;
|
||||
`;
|
||||
|
||||
export const FailedMessageContainer = styled(Tooltip)`
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
`;
|
||||
|
||||
export const ChartContainer = styled(Card)`
|
||||
border-radius: 4px;
|
||||
&&& {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 1.5rem 0;
|
||||
height: 57vh;
|
||||
/* padding-bottom: 2rem; */
|
||||
}
|
||||
`;
|
49
frontend/src/container/FormAlertRules/PromqlSection.tsx
Normal file
49
frontend/src/container/FormAlertRules/PromqlSection.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import PromQLQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query';
|
||||
import { IPromQLQueryHandleChange } from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/types';
|
||||
import React from 'react';
|
||||
import { IPromQueries } from 'types/api/alerts/compositeQuery';
|
||||
|
||||
function PromqlSection({
|
||||
promQueries,
|
||||
setPromQueries,
|
||||
}: PromqlSectionProps): JSX.Element {
|
||||
const handlePromQLQueryChange = ({
|
||||
query,
|
||||
legend,
|
||||
toggleDelete,
|
||||
}: IPromQLQueryHandleChange): void => {
|
||||
let promQuery = promQueries.A;
|
||||
|
||||
// todo(amol): how to remove query, make it null?
|
||||
if (query) promQuery.query = query;
|
||||
if (legend) promQuery.legend = legend;
|
||||
if (toggleDelete) {
|
||||
promQuery = {
|
||||
query: '',
|
||||
legend: '',
|
||||
name: 'A',
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
setPromQueries({
|
||||
A: {
|
||||
...promQuery,
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<PromQLQueryBuilder
|
||||
key="A"
|
||||
queryIndex="A"
|
||||
queryData={{ ...promQueries?.A, name: 'A' }}
|
||||
handleQueryChange={handlePromQLQueryChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface PromqlSectionProps {
|
||||
promQueries: IPromQueries;
|
||||
setPromQueries: (p: IPromQueries) => void;
|
||||
}
|
||||
|
||||
export default PromqlSection;
|
288
frontend/src/container/FormAlertRules/QuerySection.tsx
Normal file
288
frontend/src/container/FormAlertRules/QuerySection.tsx
Normal file
@ -0,0 +1,288 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { notification, Tabs } from 'antd';
|
||||
import MetricsBuilderFormula from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/formula';
|
||||
import MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query';
|
||||
import {
|
||||
IQueryBuilderFormulaHandleChange,
|
||||
IQueryBuilderQueryHandleChange,
|
||||
} from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/types';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
IFormulaQueries,
|
||||
IMetricQueries,
|
||||
IPromQueries,
|
||||
} from 'types/api/alerts/compositeQuery';
|
||||
import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import PromqlSection from './PromqlSection';
|
||||
import { FormContainer, QueryButton, StepHeading } from './styles';
|
||||
import { toIMetricsBuilderQuery } from './utils';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
function QuerySection({
|
||||
queryCategory,
|
||||
setQueryCategory,
|
||||
metricQueries,
|
||||
setMetricQueries,
|
||||
formulaQueries,
|
||||
setFormulaQueries,
|
||||
promQueries,
|
||||
setPromQueries,
|
||||
}: QuerySectionProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const handleQueryCategoryChange = (s: string): void => {
|
||||
if (
|
||||
parseInt(s, 10) === EQueryType.PROM &&
|
||||
(!promQueries || Object.keys(promQueries).length === 0)
|
||||
) {
|
||||
setPromQueries({
|
||||
A: {
|
||||
query: '',
|
||||
stats: '',
|
||||
name: 'A',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setQueryCategory(parseInt(s, 10));
|
||||
};
|
||||
|
||||
const getNextQueryLabel = useCallback((): string => {
|
||||
let maxAscii = 0;
|
||||
|
||||
Object.keys(metricQueries).forEach((key) => {
|
||||
const n = key.charCodeAt(0);
|
||||
if (n > maxAscii) {
|
||||
maxAscii = n - 64;
|
||||
}
|
||||
});
|
||||
|
||||
return String.fromCharCode(64 + maxAscii + 1);
|
||||
}, [metricQueries]);
|
||||
|
||||
const handleFormulaChange = ({
|
||||
formulaIndex,
|
||||
expression,
|
||||
toggleDisable,
|
||||
toggleDelete,
|
||||
}: IQueryBuilderFormulaHandleChange): void => {
|
||||
const allFormulas = formulaQueries;
|
||||
const current = allFormulas[formulaIndex];
|
||||
if (expression) {
|
||||
current.expression = expression;
|
||||
}
|
||||
|
||||
if (toggleDisable) {
|
||||
current.disabled = !current.disabled;
|
||||
}
|
||||
|
||||
if (toggleDelete) {
|
||||
delete allFormulas[formulaIndex];
|
||||
} else {
|
||||
allFormulas[formulaIndex] = current;
|
||||
}
|
||||
|
||||
setFormulaQueries({
|
||||
...allFormulas,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMetricQueryChange = ({
|
||||
queryIndex,
|
||||
aggregateFunction,
|
||||
metricName,
|
||||
tagFilters,
|
||||
groupBy,
|
||||
legend,
|
||||
toggleDisable,
|
||||
toggleDelete,
|
||||
}: IQueryBuilderQueryHandleChange): void => {
|
||||
const allQueries = metricQueries;
|
||||
const current = metricQueries[queryIndex];
|
||||
if (aggregateFunction) {
|
||||
current.aggregateOperator = aggregateFunction;
|
||||
}
|
||||
if (metricName) {
|
||||
current.metricName = metricName;
|
||||
}
|
||||
|
||||
if (tagFilters && current.tagFilters) {
|
||||
current.tagFilters.items = tagFilters;
|
||||
}
|
||||
|
||||
if (legend) {
|
||||
current.legend = legend;
|
||||
}
|
||||
|
||||
if (groupBy) {
|
||||
current.groupBy = groupBy;
|
||||
}
|
||||
|
||||
if (toggleDisable) {
|
||||
current.disabled = !current.disabled;
|
||||
}
|
||||
|
||||
if (toggleDelete) {
|
||||
delete allQueries[queryIndex];
|
||||
} else {
|
||||
allQueries[queryIndex] = current;
|
||||
}
|
||||
|
||||
setMetricQueries({
|
||||
...allQueries,
|
||||
});
|
||||
};
|
||||
|
||||
const addMetricQuery = useCallback(() => {
|
||||
if (Object.keys(metricQueries).length > 5) {
|
||||
notification.error({
|
||||
message: t('metric_query_max_limit'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const queryLabel = getNextQueryLabel();
|
||||
|
||||
const queries = metricQueries;
|
||||
queries[queryLabel] = {
|
||||
name: queryLabel,
|
||||
queryName: queryLabel,
|
||||
metricName: '',
|
||||
formulaOnly: false,
|
||||
aggregateOperator: EAggregateOperator.NOOP,
|
||||
legend: '',
|
||||
tagFilters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
groupBy: [],
|
||||
disabled: false,
|
||||
expression: queryLabel,
|
||||
};
|
||||
setMetricQueries({ ...queries });
|
||||
}, [t, getNextQueryLabel, metricQueries, setMetricQueries]);
|
||||
|
||||
const addFormula = useCallback(() => {
|
||||
// defaulting to F1 as only one formula is supported
|
||||
// in alert definition
|
||||
const queryLabel = 'F1';
|
||||
|
||||
const formulas = formulaQueries;
|
||||
formulas[queryLabel] = {
|
||||
queryName: queryLabel,
|
||||
name: queryLabel,
|
||||
formulaOnly: true,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
setFormulaQueries({ ...formulas });
|
||||
}, [formulaQueries, setFormulaQueries]);
|
||||
|
||||
const renderPromqlUI = (): JSX.Element => {
|
||||
return (
|
||||
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
|
||||
);
|
||||
};
|
||||
|
||||
const renderFormulaButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
|
||||
{t('button_formula')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
|
||||
const renderQueryButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
|
||||
{t('button_query')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMetricUI = (): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
{metricQueries &&
|
||||
Object.keys(metricQueries).map((key: string) => {
|
||||
// todo(amol): need to handle this in fetch
|
||||
const current = metricQueries[key];
|
||||
current.name = key;
|
||||
|
||||
return (
|
||||
<MetricsBuilder
|
||||
key={key}
|
||||
queryIndex={key}
|
||||
queryData={toIMetricsBuilderQuery(current)}
|
||||
selectedGraph="TIME_SERIES"
|
||||
handleQueryChange={handleMetricQueryChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{queryCategory !== EQueryType.PROM && renderQueryButton()}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
{formulaQueries &&
|
||||
Object.keys(formulaQueries).map((key: string) => {
|
||||
// todo(amol): need to handle this in fetch
|
||||
const current = formulaQueries[key];
|
||||
current.name = key;
|
||||
|
||||
return (
|
||||
<MetricsBuilderFormula
|
||||
key={key}
|
||||
formulaIndex={key}
|
||||
formulaData={current}
|
||||
handleFormulaChange={handleFormulaChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{queryCategory === EQueryType.QUERY_BUILDER &&
|
||||
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
|
||||
metricQueries &&
|
||||
Object.keys(metricQueries).length > 0 &&
|
||||
renderFormulaButton()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||
<FormContainer>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
defaultActiveKey={EQueryType.QUERY_BUILDER.toString()}
|
||||
activeKey={queryCategory.toString()}
|
||||
onChange={handleQueryCategoryChange}
|
||||
>
|
||||
<TabPane tab={t('tab_qb')} key={EQueryType.QUERY_BUILDER.toString()} />
|
||||
<TabPane tab={t('tab_promql')} key={EQueryType.PROM.toString()} />
|
||||
</Tabs>
|
||||
</div>
|
||||
{queryCategory === EQueryType.PROM ? renderPromqlUI() : renderMetricUI()}
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface QuerySectionProps {
|
||||
queryCategory: EQueryType;
|
||||
setQueryCategory: (n: EQueryType) => void;
|
||||
metricQueries: IMetricQueries;
|
||||
setMetricQueries: (b: IMetricQueries) => void;
|
||||
formulaQueries: IFormulaQueries;
|
||||
setFormulaQueries: (b: IFormulaQueries) => void;
|
||||
promQueries: IPromQueries;
|
||||
setPromQueries: (p: IPromQueries) => void;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
175
frontend/src/container/FormAlertRules/RuleOptions.tsx
Normal file
175
frontend/src/container/FormAlertRules/RuleOptions.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { Select, Typography } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AlertDef,
|
||||
defaultCompareOp,
|
||||
defaultEvalWindow,
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
FormContainer,
|
||||
InlineSelect,
|
||||
StepHeading,
|
||||
ThresholdInput,
|
||||
} from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function RuleOptions({
|
||||
alertDef,
|
||||
setAlertDef,
|
||||
queryCategory,
|
||||
}: RuleOptionsProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const handleMatchOptChange = (value: string | unknown): void => {
|
||||
const m = (value as string) || alertDef.condition?.matchType;
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
matchType: m,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderCompareOps = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultCompareOp}
|
||||
value={alertDef.condition?.op}
|
||||
style={{ minWidth: '120px' }}
|
||||
onChange={(value: string | unknown): void => {
|
||||
const newOp = (value as string) || '';
|
||||
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
op: newOp,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Option value="1">{t('option_above')}</Option>
|
||||
<Option value="2">{t('option_below')}</Option>
|
||||
<Option value="3">{t('option_equal')}</Option>
|
||||
<Option value="4">{t('option_notequal')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderThresholdMatchOpts = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||
>
|
||||
<Option value="1">{t('option_atleastonce')}</Option>
|
||||
<Option value="2">{t('option_allthetimes')}</Option>
|
||||
<Option value="3">{t('option_onaverage')}</Option>
|
||||
<Option value="4">{t('option_intotal')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPromMatchOpts = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||
>
|
||||
<Option value="1">{t('option_atleastonce')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEvalWindows = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultEvalWindow}
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.evalWindow}
|
||||
onChange={(value: string | unknown): void => {
|
||||
const ew = (value as string) || alertDef.evalWindow;
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
evalWindow: ew,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<Option value="5m0s">{t('option_5min')}</Option>
|
||||
<Option value="10m0s">{t('option_10min')}</Option>
|
||||
<Option value="15m0s">{t('option_15min')}</Option>
|
||||
<Option value="60m0s">{t('option_60min')}</Option>
|
||||
<Option value="4h0m0s">{t('option_4hours')}</Option>
|
||||
<Option value="24h0m0s">{t('option_24hours')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderThresholdRuleOpts = (): JSX.Element => {
|
||||
return (
|
||||
<FormItem>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
|
||||
</Typography.Text>
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
const renderPromRuleOptions = (): JSX.Element => {
|
||||
return (
|
||||
<FormItem>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderPromMatchOpts()}
|
||||
</Typography.Text>
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading>{t('alert_form_step2')}</StepHeading>
|
||||
<FormContainer>
|
||||
{queryCategory === EQueryType.PROM
|
||||
? renderPromRuleOptions()
|
||||
: renderThresholdRuleOpts()}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ThresholdInput
|
||||
controls={false}
|
||||
addonBefore={t('field_threshold')}
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={(value: number | unknown): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
target: (value as number) || undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface RuleOptionsProps {
|
||||
alertDef: AlertDef;
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
queryCategory: EQueryType;
|
||||
}
|
||||
export default RuleOptions;
|
132
frontend/src/container/FormAlertRules/UserGuide/index.tsx
Normal file
132
frontend/src/container/FormAlertRules/UserGuide/index.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import { Col, Row, Typography } from 'antd';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
StyledList,
|
||||
StyledListItem,
|
||||
StyledMainContainer,
|
||||
StyledTopic,
|
||||
} from './styles';
|
||||
|
||||
function UserGuide({ queryType }: UserGuideProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const renderStep1QB = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep2QB = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStep3QB = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderGuideForQB = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{renderStep1QB()}
|
||||
{renderStep2QB()}
|
||||
{renderStep3QB()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep1PQL = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep2PQL = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStep3PQL = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderGuideForPQL = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{renderStep1PQL()}
|
||||
{renderStep2PQL()}
|
||||
{renderStep3PQL()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledMainContainer>
|
||||
<Row>
|
||||
<Col flex="auto">
|
||||
<Typography.Paragraph> {t('user_guide_headline')} </Typography.Paragraph>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<TextToolTip
|
||||
text={t('user_tooltip_more_help')}
|
||||
url="https://signoz.io/docs/userguide/alerts-management/#create-alert-rules"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{queryType === EQueryType.QUERY_BUILDER && renderGuideForQB()}
|
||||
{queryType === EQueryType.PROM && renderGuideForPQL()}
|
||||
</StyledMainContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface UserGuideProps {
|
||||
queryType: EQueryType;
|
||||
}
|
||||
|
||||
export default UserGuide;
|
17
frontend/src/container/FormAlertRules/UserGuide/styles.ts
Normal file
17
frontend/src/container/FormAlertRules/UserGuide/styles.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Card, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const StyledMainContainer = styled(Card)``;
|
||||
|
||||
export const StyledTopic = styled(Typography.Paragraph)`
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const StyledList = styled.ul`
|
||||
padding-left: 18px;
|
||||
`;
|
||||
|
||||
export const StyledListItem = styled.li`
|
||||
font-style: italic;
|
||||
padding-bottom: 0.5rem;
|
||||
`;
|
381
frontend/src/container/FormAlertRules/index.tsx
Normal file
381
frontend/src/container/FormAlertRules/index.tsx
Normal file
@ -0,0 +1,381 @@
|
||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { FormInstance, Modal, notification, Typography } from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import ROUTES from 'constants/routes';
|
||||
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import {
|
||||
IFormulaQueries,
|
||||
IMetricQueries,
|
||||
IPromQueries,
|
||||
} from 'types/api/alerts/compositeQuery';
|
||||
import {
|
||||
AlertDef,
|
||||
defaultEvalWindow,
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import { Query as StagedQuery } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import BasicInfo from './BasicInfo';
|
||||
import ChartPreview from './ChartPreview';
|
||||
import QuerySection from './QuerySection';
|
||||
import RuleOptions from './RuleOptions';
|
||||
import {
|
||||
ActionButton,
|
||||
ButtonContainer,
|
||||
MainFormContainer,
|
||||
PanelContainer,
|
||||
StyledLeftContainer,
|
||||
StyledRightContainer,
|
||||
} from './styles';
|
||||
import useDebounce from './useDebounce';
|
||||
import UserGuide from './UserGuide';
|
||||
import {
|
||||
prepareBuilderQueries,
|
||||
prepareStagedQuery,
|
||||
toChartInterval,
|
||||
toFormulaQueries,
|
||||
toMetricQueries,
|
||||
} from './utils';
|
||||
|
||||
function FormAlertRules({
|
||||
formInstance,
|
||||
initialValue,
|
||||
ruleId,
|
||||
}: FormAlertRuleProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
// use query client
|
||||
const ruleCache = useQueryClient();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// alertDef holds the form values to be posted
|
||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||
|
||||
// initQuery contains initial query when component was mounted
|
||||
const initQuery = initialValue?.condition?.compositeMetricQuery;
|
||||
|
||||
const [queryCategory, setQueryCategory] = useState<EQueryType>(
|
||||
initQuery?.queryType,
|
||||
);
|
||||
|
||||
// local state to handle metric queries
|
||||
const [metricQueries, setMetricQueries] = useState<IMetricQueries>(
|
||||
toMetricQueries(initQuery?.builderQueries),
|
||||
);
|
||||
|
||||
// local state to handle formula queries
|
||||
const [formulaQueries, setFormulaQueries] = useState<IFormulaQueries>(
|
||||
toFormulaQueries(initQuery?.builderQueries),
|
||||
);
|
||||
|
||||
// local state to handle promql queries
|
||||
const [promQueries, setPromQueries] = useState<IPromQueries>({
|
||||
...initQuery?.promQueries,
|
||||
});
|
||||
|
||||
// staged query is used to display chart preview
|
||||
const [stagedQuery, setStagedQuery] = useState<StagedQuery>();
|
||||
const debouncedStagedQuery = useDebounce(stagedQuery, 500);
|
||||
|
||||
// this use effect initiates staged query and
|
||||
// other queries based on server data.
|
||||
// useful when fetching of initial values (from api)
|
||||
// is delayed
|
||||
useEffect(() => {
|
||||
const initQuery = initialValue?.condition?.compositeMetricQuery;
|
||||
const typ = initQuery?.queryType;
|
||||
|
||||
// extract metric query from builderQueries
|
||||
const mq = toMetricQueries(initQuery?.builderQueries);
|
||||
|
||||
// extract formula query from builderQueries
|
||||
const fq = toFormulaQueries(initQuery?.builderQueries);
|
||||
|
||||
// prepare staged query
|
||||
const sq = prepareStagedQuery(typ, mq, fq, initQuery?.promQueries);
|
||||
const pq = initQuery?.promQueries;
|
||||
|
||||
setQueryCategory(typ);
|
||||
setMetricQueries(mq);
|
||||
setFormulaQueries(fq);
|
||||
setPromQueries(pq);
|
||||
setStagedQuery(sq);
|
||||
setAlertDef(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
// this useEffect updates staging query when
|
||||
// any of its sub-parameters changes
|
||||
useEffect(() => {
|
||||
// prepare staged query
|
||||
const sq: StagedQuery = prepareStagedQuery(
|
||||
queryCategory,
|
||||
metricQueries,
|
||||
formulaQueries,
|
||||
promQueries,
|
||||
);
|
||||
setStagedQuery(sq);
|
||||
}, [queryCategory, metricQueries, formulaQueries, promQueries]);
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
}, []);
|
||||
|
||||
// onQueryCategoryChange handles changes to query category
|
||||
// in state as well as sets additional defaults
|
||||
const onQueryCategoryChange = (val: EQueryType): void => {
|
||||
setQueryCategory(val);
|
||||
if (val === EQueryType.PROM) {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
matchType: defaultMatchType,
|
||||
},
|
||||
evalWindow: defaultEvalWindow,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isFormValid = useCallback((): boolean => {
|
||||
let retval = true;
|
||||
|
||||
if (!alertDef.alert || alertDef.alert === '') {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('alertname_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
queryCategory === EQueryType.PROM &&
|
||||
(!promQueries || Object.keys(promQueries).length === 0)
|
||||
) {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('promql_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(queryCategory === EQueryType.QUERY_BUILDER && !metricQueries) ||
|
||||
Object.keys(metricQueries).length === 0
|
||||
) {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('condition_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.keys(metricQueries).forEach((key) => {
|
||||
if (metricQueries[key].metricName === '') {
|
||||
retval = false;
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('metricname_missing', { where: metricQueries[key].name }),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(formulaQueries).forEach((key) => {
|
||||
if (formulaQueries[key].expression === '') {
|
||||
retval = false;
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('expression_missing', formulaQueries[key].name),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return retval;
|
||||
}, [t, alertDef, queryCategory, metricQueries, formulaQueries, promQueries]);
|
||||
|
||||
const saveRule = useCallback(async () => {
|
||||
if (!isFormValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postableAlert: AlertDef = {
|
||||
...alertDef,
|
||||
source: window?.location.toString(),
|
||||
ruleType:
|
||||
queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule',
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
compositeMetricQuery: {
|
||||
builderQueries: prepareBuilderQueries(metricQueries, formulaQueries),
|
||||
promQueries,
|
||||
queryType: queryCategory,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const apiReq =
|
||||
ruleId && ruleId > 0
|
||||
? { data: postableAlert, id: ruleId }
|
||||
: { data: postableAlert };
|
||||
|
||||
const response = await saveAlertApi(apiReq);
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notification.success({
|
||||
message: 'Success',
|
||||
description:
|
||||
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
|
||||
});
|
||||
console.log('invalidting cache');
|
||||
// invalidate rule in cache
|
||||
ruleCache.invalidateQueries(['ruleId', ruleId]);
|
||||
|
||||
setTimeout(() => {
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
}, 2000);
|
||||
} else {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('save alert api failed:', e);
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}, [
|
||||
t,
|
||||
isFormValid,
|
||||
queryCategory,
|
||||
ruleId,
|
||||
alertDef,
|
||||
metricQueries,
|
||||
formulaQueries,
|
||||
promQueries,
|
||||
ruleCache,
|
||||
]);
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
const content = (
|
||||
<Typography.Text>
|
||||
{' '}
|
||||
{t('confirm_save_content_part1')} <QueryTypeTag queryType={queryCategory} />{' '}
|
||||
{t('confirm_save_content_part2')}
|
||||
</Typography.Text>
|
||||
);
|
||||
Modal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: t('confirm_save_title'),
|
||||
centered: true,
|
||||
content,
|
||||
onOk() {
|
||||
saveRule();
|
||||
},
|
||||
});
|
||||
}, [t, saveRule, queryCategory]);
|
||||
|
||||
const renderBasicInfo = (): JSX.Element => (
|
||||
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
||||
);
|
||||
|
||||
const renderQBChartPreview = (): JSX.Element => {
|
||||
return (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name=""
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPromChartPreview = (): JSX.Element => {
|
||||
return (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
<PanelContainer>
|
||||
<StyledLeftContainer flex="5 1 600px">
|
||||
<MainFormContainer
|
||||
initialValues={initialValue}
|
||||
layout="vertical"
|
||||
form={formInstance}
|
||||
>
|
||||
{queryCategory === EQueryType.QUERY_BUILDER && renderQBChartPreview()}
|
||||
{queryCategory === EQueryType.PROM && renderPromChartPreview()}
|
||||
<QuerySection
|
||||
queryCategory={queryCategory}
|
||||
setQueryCategory={onQueryCategoryChange}
|
||||
metricQueries={metricQueries}
|
||||
setMetricQueries={setMetricQueries}
|
||||
formulaQueries={formulaQueries}
|
||||
setFormulaQueries={setFormulaQueries}
|
||||
promQueries={promQueries}
|
||||
setPromQueries={setPromQueries}
|
||||
/>
|
||||
|
||||
<RuleOptions
|
||||
queryCategory={queryCategory}
|
||||
alertDef={alertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
/>
|
||||
|
||||
{renderBasicInfo()}
|
||||
<ButtonContainer>
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
>
|
||||
{ruleId > 0 ? t('button_savechanges') : t('button_createrule')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
disabled={loading || false}
|
||||
type="default"
|
||||
onClick={onCancelHandler}
|
||||
>
|
||||
{ruleId === 0 && t('button_cancelchanges')}
|
||||
{ruleId > 0 && t('button_discard')}
|
||||
</ActionButton>
|
||||
</ButtonContainer>
|
||||
</MainFormContainer>
|
||||
</StyledLeftContainer>
|
||||
<StyledRightContainer flex="1 1 300px">
|
||||
<UserGuide queryType={queryCategory} />
|
||||
</StyledRightContainer>
|
||||
</PanelContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface FormAlertRuleProps {
|
||||
formInstance: FormInstance;
|
||||
initialValue: AlertDef;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default FormAlertRules;
|
@ -0,0 +1,49 @@
|
||||
import { createMachine } from 'xstate';
|
||||
|
||||
export const ResourceAttributesFilterMachine =
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */
|
||||
createMachine({
|
||||
tsTypes: {} as import('./Labels.machine.typegen').Typegen0,
|
||||
initial: 'Idle',
|
||||
states: {
|
||||
LabelKey: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectLabelValue',
|
||||
target: 'LabelValue',
|
||||
},
|
||||
onBlur: {
|
||||
actions: 'onSelectLabelValue',
|
||||
target: 'LabelValue',
|
||||
},
|
||||
RESET: {
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
LabelValue: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: ['onValidateQuery'],
|
||||
},
|
||||
onBlur: {
|
||||
actions: ['onValidateQuery'],
|
||||
// target: 'Idle',
|
||||
},
|
||||
RESET: {
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
Idle: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectLabelKey',
|
||||
description: 'Enter a label key',
|
||||
target: 'LabelKey',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
id: 'Label Key Values',
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
// This file was automatically generated. Edits will be overwritten
|
||||
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true;
|
||||
eventsCausingActions: {
|
||||
onSelectLabelValue: 'NEXT' | 'onBlur';
|
||||
onValidateQuery: 'NEXT' | 'onBlur';
|
||||
onSelectLabelKey: 'NEXT';
|
||||
};
|
||||
internalEvents: {
|
||||
'xstate.init': { type: 'xstate.init' };
|
||||
};
|
||||
invokeSrcNameMap: {};
|
||||
missingImplementations: {
|
||||
actions: 'onSelectLabelValue' | 'onValidateQuery' | 'onSelectLabelKey';
|
||||
services: never;
|
||||
guards: never;
|
||||
delays: never;
|
||||
};
|
||||
eventsCausingServices: {};
|
||||
eventsCausingGuards: {};
|
||||
eventsCausingDelays: {};
|
||||
matchesStates: 'LabelKey' | 'LabelValue' | 'Idle';
|
||||
tags: never;
|
||||
}
|
26
frontend/src/container/FormAlertRules/labels/QueryChip.tsx
Normal file
26
frontend/src/container/FormAlertRules/labels/QueryChip.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import { QueryChipContainer, QueryChipItem } from './styles';
|
||||
import { ILabelRecord } from './types';
|
||||
|
||||
interface QueryChipProps {
|
||||
queryData: ILabelRecord;
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function QueryChip({
|
||||
queryData,
|
||||
onRemove,
|
||||
}: QueryChipProps): JSX.Element {
|
||||
const { key, value } = queryData;
|
||||
return (
|
||||
<QueryChipContainer>
|
||||
<QueryChipItem
|
||||
closable={key !== 'severity' && key !== 'description'}
|
||||
onClose={(): void => onRemove(key)}
|
||||
>
|
||||
{key}: {value}
|
||||
</QueryChipItem>
|
||||
</QueryChipContainer>
|
||||
);
|
||||
}
|
164
frontend/src/container/FormAlertRules/labels/index.tsx
Normal file
164
frontend/src/container/FormAlertRules/labels/index.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import {
|
||||
CloseCircleFilled,
|
||||
ExclamationCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { Button, Input, message, Modal } from 'antd';
|
||||
import { map } from 'lodash-es';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ResourceAttributesFilterMachine } from './Labels.machine';
|
||||
import QueryChip from './QueryChip';
|
||||
import { QueryChipItem, SearchContainer } from './styles';
|
||||
import { ILabelRecord } from './types';
|
||||
import { createQuery, flattenLabels, prepareLabels } from './utils';
|
||||
|
||||
interface LabelSelectProps {
|
||||
onSetLabels: (q: Labels) => void;
|
||||
initialValues: Labels | undefined;
|
||||
}
|
||||
|
||||
function LabelSelect({
|
||||
onSetLabels,
|
||||
initialValues,
|
||||
}: LabelSelectProps): JSX.Element | null {
|
||||
const { t } = useTranslation('alerts');
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const [currentVal, setCurrentVal] = useState('');
|
||||
const [staging, setStaging] = useState<string[]>([]);
|
||||
const [queries, setQueries] = useState<ILabelRecord[]>(
|
||||
initialValues ? flattenLabels(initialValues) : [],
|
||||
);
|
||||
|
||||
const dispatchChanges = (updatedRecs: ILabelRecord[]): void => {
|
||||
onSetLabels(prepareLabels(updatedRecs, initialValues));
|
||||
setQueries(updatedRecs);
|
||||
};
|
||||
|
||||
const [state, send] = useMachine(ResourceAttributesFilterMachine, {
|
||||
actions: {
|
||||
onSelectLabelKey: () => {},
|
||||
onSelectLabelValue: () => {
|
||||
if (currentVal !== '') {
|
||||
setStaging((prevState) => [...prevState, currentVal]);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
setCurrentVal('');
|
||||
},
|
||||
onValidateQuery: (): void => {
|
||||
if (currentVal === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedQuery = createQuery([...staging, currentVal]);
|
||||
|
||||
if (generatedQuery) {
|
||||
dispatchChanges([...queries, generatedQuery]);
|
||||
setStaging([]);
|
||||
setCurrentVal('');
|
||||
send('RESET');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleFocus = (): void => {
|
||||
if (state.value === 'Idle') {
|
||||
send('NEXT');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = useCallback((): void => {
|
||||
if (staging.length === 1 && staging[0] !== undefined) {
|
||||
send('onBlur');
|
||||
}
|
||||
}, [send, staging]);
|
||||
|
||||
useEffect(() => {
|
||||
handleBlur();
|
||||
}, [handleBlur]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setCurrentVal(e.target?.value);
|
||||
};
|
||||
|
||||
const handleClose = (key: string): void => {
|
||||
dispatchChanges(queries.filter((queryData) => queryData.key !== key));
|
||||
};
|
||||
|
||||
const handleClearAll = (): void => {
|
||||
Modal.confirm({
|
||||
title: 'Confirm',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: t('remove_label_confirm'),
|
||||
onOk() {
|
||||
send('RESET');
|
||||
dispatchChanges([]);
|
||||
setStaging([]);
|
||||
message.success(t('remove_label_success'));
|
||||
},
|
||||
okText: t('button_yes'),
|
||||
cancelText: t('button_no'),
|
||||
});
|
||||
};
|
||||
const renderPlaceholder = useCallback((): string => {
|
||||
if (state.value === 'LabelKey') return 'Enter a label key then press ENTER.';
|
||||
if (state.value === 'LabelValue')
|
||||
return `Enter a value for label key(${staging[0]}) then press ENTER.`;
|
||||
return t('placeholder_label_key_pair');
|
||||
}, [t, state, staging]);
|
||||
return (
|
||||
<SearchContainer isDarkMode={isDarkMode} disabled={false}>
|
||||
<div style={{ display: 'inline-flex', flexWrap: 'wrap' }}>
|
||||
{queries.length > 0 &&
|
||||
map(
|
||||
queries,
|
||||
(query): JSX.Element => {
|
||||
return (
|
||||
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{map(staging, (item) => {
|
||||
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<Input
|
||||
placeholder={renderPlaceholder()}
|
||||
onChange={handleChange}
|
||||
onKeyUp={(e): void => {
|
||||
if (e.key === 'Enter' || e.code === 'Enter') {
|
||||
send('NEXT');
|
||||
}
|
||||
}}
|
||||
bordered={false}
|
||||
value={currentVal as never}
|
||||
style={{ flex: 1 }}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
|
||||
{queries.length || staging.length || currentVal ? (
|
||||
<Button
|
||||
onClick={handleClearAll}
|
||||
icon={<CloseCircleFilled />}
|
||||
type="text"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</SearchContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default LabelSelect;
|
35
frontend/src/container/FormAlertRules/labels/styles.ts
Normal file
35
frontend/src/container/FormAlertRules/labels/styles.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { grey } from '@ant-design/colors';
|
||||
import { Tag } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface SearchContainerProps {
|
||||
isDarkMode: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const SearchContainer = styled.div<SearchContainerProps>`
|
||||
width: 70%;
|
||||
border-radisu: 4px;
|
||||
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#fff')};
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.2rem;
|
||||
border: 1px solid #ccc5;
|
||||
${({ disabled }): string => (disabled ? `cursor: not-allowed;` : '')}
|
||||
`;
|
||||
|
||||
export const QueryChipContainer = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
&:hover {
|
||||
& > * {
|
||||
background: ${grey.primary}44;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QueryChipItem = styled(Tag)`
|
||||
margin-right: 0.1rem;
|
||||
`;
|
9
frontend/src/container/FormAlertRules/labels/types.ts
Normal file
9
frontend/src/container/FormAlertRules/labels/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface ILabelRecord {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
54
frontend/src/container/FormAlertRules/labels/utils.ts
Normal file
54
frontend/src/container/FormAlertRules/labels/utils.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
|
||||
import { ILabelRecord } from './types';
|
||||
|
||||
const hiddenLabels = ['severity', 'description'];
|
||||
|
||||
export const createQuery = (
|
||||
selectedItems: Array<string | string[]> = [],
|
||||
): ILabelRecord | null => {
|
||||
if (selectedItems.length === 2) {
|
||||
return {
|
||||
key: selectedItems[0] as string,
|
||||
value: selectedItems[1] as string,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const flattenLabels = (labels: Labels): ILabelRecord[] => {
|
||||
const recs: ILabelRecord[] = [];
|
||||
|
||||
Object.keys(labels).forEach((key) => {
|
||||
if (!hiddenLabels.includes(key)) {
|
||||
recs.push({
|
||||
key,
|
||||
value: labels[key],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return recs;
|
||||
};
|
||||
|
||||
export const prepareLabels = (
|
||||
recs: ILabelRecord[],
|
||||
alertLabels: Labels | undefined,
|
||||
): Labels => {
|
||||
const labels: Labels = {};
|
||||
|
||||
recs.forEach((rec) => {
|
||||
if (!hiddenLabels.includes(rec.key)) {
|
||||
labels[rec.key] = rec.value;
|
||||
}
|
||||
});
|
||||
if (alertLabels) {
|
||||
Object.keys(alertLabels).forEach((key) => {
|
||||
if (hiddenLabels.includes(key)) {
|
||||
labels[key] = alertLabels[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
};
|
101
frontend/src/container/FormAlertRules/styles.ts
Normal file
101
frontend/src/container/FormAlertRules/styles.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { Button, Card, Col, Form, Input, InputNumber, Row, Select } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const PanelContainer = styled(Row)`
|
||||
flex-wrap: nowrap;
|
||||
`;
|
||||
|
||||
export const StyledRightContainer = styled(Col)`
|
||||
&&& {
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledLeftContainer = styled(Col)`
|
||||
&&& {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const MainFormContainer = styled(Form)``;
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ActionButton = styled(Button)`
|
||||
margin-right: 1rem;
|
||||
`;
|
||||
|
||||
export const QueryButton = styled(Button)`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const QueryContainer = styled(Card)`
|
||||
&&& {
|
||||
margin-top: 1rem;
|
||||
min-height: 23.5%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const StepHeading = styled.p`
|
||||
margin-top: 1rem;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
export const InlineSelect = styled(Select)`
|
||||
display: inline-block;
|
||||
width: 10% !important;
|
||||
margin-left: 0.2em;
|
||||
margin-right: 0.2em;
|
||||
`;
|
||||
|
||||
export const SeveritySelect = styled(Select)`
|
||||
width: 15% !important;
|
||||
`;
|
||||
|
||||
export const InputSmall = styled(Input)`
|
||||
width: 40% !important;
|
||||
`;
|
||||
|
||||
export const FormContainer = styled(Card)`
|
||||
padding: 2em;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
export const ThresholdInput = styled(InputNumber)`
|
||||
& > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > .ant-input-number-group-addon {
|
||||
width: 130px;
|
||||
}
|
||||
& > .ant-input-number {
|
||||
width: 50%;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TextareaMedium = styled(TextArea)`
|
||||
width: 70%;
|
||||
`;
|
31
frontend/src/container/FormAlertRules/useDebounce.js
Normal file
31
frontend/src/container/FormAlertRules/useDebounce.js
Normal file
@ -0,0 +1,31 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// see https://github.com/tannerlinsley/react-query/issues/293
|
||||
// see https://usehooks.com/useDebounce/
|
||||
export default function useDebounce(value, delay) {
|
||||
// State and setters for debounced value
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
},
|
||||
[value, delay] // Only re-call effect if value or delay changes
|
||||
);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
136
frontend/src/container/FormAlertRules/utils.ts
Normal file
136
frontend/src/container/FormAlertRules/utils.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
IBuilderQueries,
|
||||
IFormulaQueries,
|
||||
IFormulaQuery,
|
||||
IMetricQueries,
|
||||
IMetricQuery,
|
||||
IPromQueries,
|
||||
IPromQuery,
|
||||
} from 'types/api/alerts/compositeQuery';
|
||||
import {
|
||||
IMetricsBuilderQuery,
|
||||
Query as IStagedQuery,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const toFormulaQueries = (b: IBuilderQueries): IFormulaQueries => {
|
||||
const f: IFormulaQueries = {};
|
||||
if (!b) return f;
|
||||
Object.keys(b).forEach((key) => {
|
||||
if (key === 'F1') {
|
||||
f[key] = b[key] as IFormulaQuery;
|
||||
}
|
||||
});
|
||||
|
||||
return f;
|
||||
};
|
||||
|
||||
export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
|
||||
const m: IMetricQueries = {};
|
||||
if (!b) return m;
|
||||
Object.keys(b).forEach((key) => {
|
||||
if (key !== 'F1') {
|
||||
m[key] = b[key] as IMetricQuery;
|
||||
}
|
||||
});
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
export const toIMetricsBuilderQuery = (
|
||||
q: IMetricQuery,
|
||||
): IMetricsBuilderQuery => {
|
||||
return {
|
||||
name: q.name,
|
||||
metricName: q.metricName,
|
||||
tagFilters: q.tagFilters,
|
||||
groupBy: q.groupBy,
|
||||
aggregateOperator: q.aggregateOperator,
|
||||
disabled: q.disabled,
|
||||
legend: q.legend,
|
||||
};
|
||||
};
|
||||
|
||||
export const prepareBuilderQueries = (
|
||||
m: IMetricQueries,
|
||||
f: IFormulaQueries,
|
||||
): IBuilderQueries => {
|
||||
if (!m) return {};
|
||||
const b: IBuilderQueries = {
|
||||
...m,
|
||||
};
|
||||
|
||||
Object.keys(f).forEach((key) => {
|
||||
b[key] = {
|
||||
...f[key],
|
||||
aggregateOperator: undefined,
|
||||
metricName: '',
|
||||
};
|
||||
});
|
||||
return b;
|
||||
};
|
||||
|
||||
export const prepareStagedQuery = (
|
||||
t: EQueryType,
|
||||
m: IMetricQueries,
|
||||
f: IFormulaQueries,
|
||||
p: IPromQueries,
|
||||
): IStagedQuery => {
|
||||
const qbList: IMetricQuery[] = [];
|
||||
const formulaList: IFormulaQuery[] = [];
|
||||
const promList: IPromQuery[] = [];
|
||||
|
||||
// convert map[string]IMetricQuery to IMetricQuery[]
|
||||
if (m) {
|
||||
Object.keys(m).forEach((key) => {
|
||||
qbList.push(m[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// convert map[string]IFormulaQuery to IFormulaQuery[]
|
||||
if (f) {
|
||||
Object.keys(f).forEach((key) => {
|
||||
formulaList.push(f[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// convert map[string]IPromQuery to IPromQuery[]
|
||||
if (p) {
|
||||
Object.keys(p).forEach((key) => {
|
||||
promList.push({ ...p[key], name: key });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
queryType: t,
|
||||
promQL: promList,
|
||||
metricsBuilder: {
|
||||
formulas: formulaList,
|
||||
queryBuilder: qbList,
|
||||
},
|
||||
clickHouse: [],
|
||||
};
|
||||
};
|
||||
|
||||
// toChartInterval converts eval window to chart selection time interval
|
||||
export const toChartInterval = (evalWindow: string | undefined): Time => {
|
||||
switch (evalWindow) {
|
||||
case '5m0s':
|
||||
return '5min';
|
||||
case '10m0s':
|
||||
return '10min';
|
||||
case '15m0s':
|
||||
return '15min';
|
||||
case '30m0s':
|
||||
return '30min';
|
||||
case '60m0s':
|
||||
return '30min';
|
||||
case '4h0m0s':
|
||||
return '4hr';
|
||||
case '24h0m0s':
|
||||
return '1day';
|
||||
default:
|
||||
return '5min';
|
||||
}
|
||||
};
|
@ -10,7 +10,7 @@ function SpanNameComponent({
|
||||
<Container title={`${name} ${serviceName}`}>
|
||||
<SpanWrapper>
|
||||
<Span ellipsis>{name}</Span>
|
||||
<Service>{serviceName}</Service>
|
||||
<Service ellipsis>{serviceName}</Service>
|
||||
</SpanWrapper>
|
||||
</Container>
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ export const Span = styled(Typography.Paragraph)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const Service = styled(Typography)`
|
||||
export const Service = styled(Typography.Paragraph)`
|
||||
&&& {
|
||||
color: #acacac;
|
||||
font-size: 0.75rem;
|
||||
|
@ -41,8 +41,9 @@ export const CardContainer = styled.li<{ isMissing?: boolean }>`
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
z-index: 2;
|
||||
${({ isMissing }): string =>
|
||||
isMissing ? `border: 1px dashed ${volcano[6]};` : ''}
|
||||
isMissing ? `border: 1px dashed ${volcano[6]} !important;` : ''}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Typography } from 'antd';
|
||||
import { ChartData } from 'chart.js';
|
||||
import Graph, { GraphOnClickHandler } from 'components/Graph';
|
||||
import Graph, { GraphOnClickHandler, StaticLineProps } from 'components/Graph';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import ValueGraph from 'components/ValueGraph';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
@ -18,6 +18,7 @@ function GridGraphComponent({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
}: GridGraphComponentProps): JSX.Element | null {
|
||||
const location = history.location.pathname;
|
||||
|
||||
@ -36,6 +37,7 @@ function GridGraphComponent({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -82,6 +84,7 @@ export interface GridGraphComponentProps {
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
staticLine?: StaticLineProps;
|
||||
}
|
||||
|
||||
GridGraphComponent.defaultProps = {
|
||||
@ -90,6 +93,7 @@ GridGraphComponent.defaultProps = {
|
||||
isStacked: undefined,
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
staticLine: undefined,
|
||||
};
|
||||
|
||||
export default GridGraphComponent;
|
||||
|
@ -64,9 +64,14 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
},
|
||||
{
|
||||
title: 'Alert Name',
|
||||
dataIndex: 'name',
|
||||
dataIndex: 'alert',
|
||||
key: 'name',
|
||||
sorter: (a, b): number => a.name.charCodeAt(0) - b.name.charCodeAt(0),
|
||||
render: (value, record): JSX.Element => (
|
||||
<Typography.Link onClick={(): void => onEditHandler(record.id.toString())}>
|
||||
{value}
|
||||
</Typography.Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Severity',
|
||||
@ -83,7 +88,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
title: 'Labels',
|
||||
dataIndex: 'labels',
|
||||
key: 'tags',
|
||||
align: 'center',
|
||||
@ -100,7 +105,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
{withOutSeverityKeys.map((e) => {
|
||||
return (
|
||||
<Tag key={e} color="magenta">
|
||||
{e}
|
||||
{e}: {value[e]}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
|
@ -25,7 +25,7 @@ function DBCall({ getWidget }: DBCallProps): JSX.Element {
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_db_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[1m])) by (db_system)`,
|
||||
query: `sum(rate(signoz_db_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (db_system)`,
|
||||
legend: '{{db_system}}',
|
||||
},
|
||||
])}
|
||||
|
@ -14,7 +14,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
||||
const { resourceAttributePromQLQuery } = useSelector<AppState, MetricReducer>(
|
||||
(state) => state.metrics,
|
||||
);
|
||||
const legend = '{{http_url}}';
|
||||
const legend = '{{address}}';
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -28,7 +28,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `max((sum(rate(signoz_external_call_latency_count{service_name="${servicename}", status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[1m]) OR rate(signoz_external_call_latency_count{service_name="${servicename}", http_status_code=~"5.."${resourceAttributePromQLQuery}}[1m]) OR vector(0)) by (http_url))*100/sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[1m])) by (http_url)) < 1000 OR vector(0)`,
|
||||
query: `max((sum(rate(signoz_external_call_latency_count{service_name="${servicename}", status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[5m]) OR vector(0)) by (address))*100/sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (address)) < 1000 OR vector(0)`,
|
||||
legend: 'External Call Error Percentage',
|
||||
},
|
||||
])}
|
||||
@ -68,7 +68,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (http_url)`,
|
||||
query: `sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (address)`,
|
||||
legend,
|
||||
},
|
||||
])}
|
||||
@ -87,7 +87,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `(sum(rate(signoz_external_call_latency_sum{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (http_url))/(sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (http_url))`,
|
||||
query: `(sum(rate(signoz_external_call_latency_sum{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (address))/(sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (address))`,
|
||||
legend,
|
||||
},
|
||||
])}
|
||||
|
@ -193,7 +193,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
}}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_latency_count{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"${resourceAttributePromQLQuery}}[2m]))`,
|
||||
query: `sum(rate(signoz_latency_count{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"${resourceAttributePromQLQuery}}[5m]))`,
|
||||
legend: 'Requests',
|
||||
},
|
||||
])}
|
||||
@ -227,7 +227,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
}}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `max(sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[1m]) OR rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", http_status_code=~"5.."${resourceAttributePromQLQuery}}[1m]))*100/sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"${resourceAttributePromQLQuery}}[1m]))) < 1000 OR vector(0)`,
|
||||
query: `max(sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[5m]) OR rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", http_status_code=~"5.."${resourceAttributePromQLQuery}}[5m]))*100/sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"${resourceAttributePromQLQuery}}[5m]))) < 1000 OR vector(0)`,
|
||||
legend: 'Error Percentage',
|
||||
},
|
||||
])}
|
||||
|
@ -56,7 +56,7 @@ function Metrics(): JSX.Element {
|
||||
render: (value: number): string => (value / 1000000).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: 'Error Rate (in %)',
|
||||
title: 'Error Rate (% of requests)',
|
||||
dataIndex: 'errorRate',
|
||||
key: 'errorRate',
|
||||
sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate,
|
||||
|
@ -29,7 +29,7 @@ function PromQLQueryContainer({
|
||||
toggleDelete,
|
||||
}: IPromQLQueryHandleChange): void => {
|
||||
const allQueries = queryData[WIDGET_PROMQL_QUERY_KEY_NAME];
|
||||
const currentIndexQuery = allQueries[queryIndex];
|
||||
const currentIndexQuery = allQueries[queryIndex as number];
|
||||
if (query !== undefined) currentIndexQuery.query = query;
|
||||
if (legend !== undefined) currentIndexQuery.legend = legend;
|
||||
|
||||
@ -37,7 +37,7 @@ function PromQLQueryContainer({
|
||||
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
||||
}
|
||||
if (toggleDelete) {
|
||||
allQueries.splice(queryIndex, 1);
|
||||
allQueries.splice(queryIndex as number, 1);
|
||||
}
|
||||
updateQueryData({ updatedQuery: { ...queryData } });
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import { IPromQLQueryHandleChange } from './types';
|
||||
|
||||
interface IPromQLQueryBuilderProps {
|
||||
queryData: IPromQLQuery;
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
handleQueryChange: (args: IPromQLQueryHandleChange) => void;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { IPromQLQuery } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface IPromQLQueryHandleChange {
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
query?: IPromQLQuery['query'];
|
||||
legend?: IPromQLQuery['legend'];
|
||||
toggleDisable?: IPromQLQuery['disabled'];
|
||||
|
@ -9,7 +9,7 @@ const { TextArea } = Input;
|
||||
|
||||
interface IMetricsBuilderFormulaProps {
|
||||
formulaData: IMetricsBuilderFormula;
|
||||
formulaIndex: number;
|
||||
formulaIndex: number | string;
|
||||
handleFormulaChange: (args: IQueryBuilderFormulaHandleChange) => void;
|
||||
}
|
||||
function MetricsBuilderFormula({
|
||||
|
@ -50,7 +50,7 @@ function QueryBuilderQueryContainer({
|
||||
}: IQueryBuilderQueryHandleChange): void => {
|
||||
const allQueries =
|
||||
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME].queryBuilder;
|
||||
const currentIndexQuery = allQueries[queryIndex];
|
||||
const currentIndexQuery = allQueries[queryIndex as number];
|
||||
if (aggregateFunction) {
|
||||
currentIndexQuery.aggregateOperator = aggregateFunction;
|
||||
}
|
||||
@ -78,7 +78,7 @@ function QueryBuilderQueryContainer({
|
||||
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
||||
}
|
||||
if (toggleDelete) {
|
||||
allQueries.splice(queryIndex, 1);
|
||||
allQueries.splice(queryIndex as number, 1);
|
||||
}
|
||||
updateQueryData({ updatedQuery: { ...queryData } });
|
||||
};
|
||||
@ -92,7 +92,7 @@ function QueryBuilderQueryContainer({
|
||||
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME][
|
||||
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME
|
||||
];
|
||||
const currentIndexFormula = allFormulas[formulaIndex];
|
||||
const currentIndexFormula = allFormulas[formulaIndex as number];
|
||||
|
||||
if (expression) {
|
||||
currentIndexFormula.expression = expression;
|
||||
@ -103,7 +103,7 @@ function QueryBuilderQueryContainer({
|
||||
}
|
||||
|
||||
if (toggleDelete) {
|
||||
allFormulas.splice(formulaIndex, 1);
|
||||
allFormulas.splice(formulaIndex as number, 1);
|
||||
}
|
||||
updateQueryData({ updatedQuery: { ...queryData } });
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ import { IQueryBuilderQueryHandleChange } from './types';
|
||||
const { Option } = Select;
|
||||
|
||||
interface IMetricsBuilderProps {
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
selectedGraph: GRAPH_TYPES;
|
||||
queryData: IMetricsBuilderQuery;
|
||||
handleQueryChange: (args: IQueryBuilderQueryHandleChange) => void;
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
} from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface IQueryBuilderQueryHandleChange {
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
aggregateFunction?: IMetricsBuilderQuery['aggregateOperator'];
|
||||
metricName?: IMetricsBuilderQuery['metricName'];
|
||||
tagFilters?: IMetricsBuilderQuery['tagFilters']['items'];
|
||||
@ -16,7 +16,7 @@ export interface IQueryBuilderQueryHandleChange {
|
||||
}
|
||||
|
||||
export interface IQueryBuilderFormulaHandleChange {
|
||||
formulaIndex: number;
|
||||
formulaIndex: number | string;
|
||||
expression?: IMetricsBuilderFormula['expression'];
|
||||
toggleDisable?: IMetricsBuilderFormula['disabled'];
|
||||
toggleDelete?: boolean;
|
||||
|
@ -1,20 +1,24 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
type FiveMin = '5min';
|
||||
type TenMin = '10min';
|
||||
type FifteenMin = '15min';
|
||||
type ThirtyMin = '30min';
|
||||
type OneMin = '1min';
|
||||
type SixHour = '6hr';
|
||||
type OneHour = '1hr';
|
||||
type FourHour = '4hr';
|
||||
type OneDay = '1day';
|
||||
type OneWeek = '1week';
|
||||
type Custom = 'custom';
|
||||
|
||||
export type Time =
|
||||
| FiveMin
|
||||
| TenMin
|
||||
| FifteenMin
|
||||
| ThirtyMin
|
||||
| OneMin
|
||||
| FourHour
|
||||
| SixHour
|
||||
| OneHour
|
||||
| Custom
|
||||
|
@ -19,6 +19,9 @@ const routesToSkip = [
|
||||
ROUTES.ALL_DASHBOARD,
|
||||
ROUTES.ORG_SETTINGS,
|
||||
ROUTES.ERROR_DETAIL,
|
||||
ROUTES.ALERTS_NEW,
|
||||
ROUTES.EDIT_ALERTS,
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
];
|
||||
|
||||
function TopNav(): JSX.Element | null {
|
||||
|
@ -9,7 +9,9 @@ export const AllTraceFilterEnum: TraceFilterEnum[] = [
|
||||
'serviceName',
|
||||
'operation',
|
||||
'component',
|
||||
'httpCode',
|
||||
'rpcMethod',
|
||||
'responseStatusCode',
|
||||
// 'httpCode',
|
||||
'httpHost',
|
||||
'httpMethod',
|
||||
'httpRoute',
|
||||
|
@ -38,6 +38,14 @@ export const groupBy: Dropdown[] = [
|
||||
displayValue: 'HTTP status code',
|
||||
key: 'httpCode',
|
||||
},
|
||||
{
|
||||
displayValue: 'RPC Method',
|
||||
key: 'rpcMethod',
|
||||
},
|
||||
{
|
||||
displayValue: 'Status Code',
|
||||
key: 'responseStatusCode',
|
||||
},
|
||||
{
|
||||
displayValue: 'Database name',
|
||||
key: 'dbName',
|
||||
|
@ -28,11 +28,10 @@ function MissingSpansMessage(): JSX.Element {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: '1rem 0',
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: volcano[6], fontSize: '1.4rem', marginRight: '0.3rem' }}
|
||||
/>{' '}
|
||||
<InfoCircleOutlined style={{ color: volcano[6], marginRight: '0.3rem' }} />{' '}
|
||||
This trace has missing spans
|
||||
</div>
|
||||
</Popover>
|
||||
|
@ -0,0 +1,53 @@
|
||||
import { StyledButton } from 'components/Styled';
|
||||
import React from 'react';
|
||||
|
||||
import { styles } from './styles';
|
||||
|
||||
function EllipsedButton({
|
||||
onToggleHandler,
|
||||
setText,
|
||||
value,
|
||||
event,
|
||||
buttonText,
|
||||
}: Props): JSX.Element {
|
||||
const isFullValueButton = buttonText === 'View full value';
|
||||
|
||||
const style = [styles.removePadding];
|
||||
|
||||
if (!isFullValueButton) {
|
||||
style.push(styles.removeMargin);
|
||||
} else {
|
||||
style.push(styles.selectedSpanDetailsContainer);
|
||||
style.push(styles.buttonContainer);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
styledclass={style}
|
||||
onClick={(): void => {
|
||||
onToggleHandler(true);
|
||||
setText({
|
||||
subText: value,
|
||||
text: event,
|
||||
});
|
||||
}}
|
||||
type="link"
|
||||
>
|
||||
{buttonText}
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onToggleHandler: (isOpen: boolean) => void;
|
||||
setText: (text: { subText: string; text: string }) => void;
|
||||
value: string;
|
||||
event: string;
|
||||
buttonText?: string;
|
||||
}
|
||||
|
||||
EllipsedButton.defaultProps = {
|
||||
buttonText: 'View full log event message',
|
||||
};
|
||||
|
||||
export default EllipsedButton;
|
@ -1,29 +1,22 @@
|
||||
import { Collapse, Modal } from 'antd';
|
||||
import Editor from 'components/Editor';
|
||||
import { StyledButton } from 'components/Styled';
|
||||
import { Collapse } from 'antd';
|
||||
import useThemeMode from 'hooks/useThemeMode';
|
||||
import keys from 'lodash-es/keys';
|
||||
import map from 'lodash-es/map';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
|
||||
import { CustomSubText, CustomSubTitle, styles } from './styles';
|
||||
import EllipsedButton from './EllipsedButton';
|
||||
import { CustomSubText, CustomSubTitle } from './styles';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
function ErrorTag({ event }: ErrorTagProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
function ErrorTag({
|
||||
event,
|
||||
onToggleHandler,
|
||||
setText,
|
||||
}: ErrorTagProps): JSX.Element {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
|
||||
const [text, setText] = useState({
|
||||
text: '',
|
||||
subText: '',
|
||||
});
|
||||
|
||||
const onToggleHandler = (state: boolean): void => {
|
||||
setIsOpen(state);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{map(event, ({ attributeMap, name }) => {
|
||||
@ -45,23 +38,23 @@ function ErrorTag({ event }: ErrorTagProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<CustomSubTitle>{event}</CustomSubTitle>
|
||||
<CustomSubText ellipsis={isEllipsed} isDarkMode={isDarkMode}>
|
||||
<CustomSubText
|
||||
ellipsis={{
|
||||
rows: isEllipsed ? 1 : 0,
|
||||
}}
|
||||
isDarkMode={isDarkMode}
|
||||
>
|
||||
{value}
|
||||
<br />
|
||||
{isEllipsed && (
|
||||
<StyledButton
|
||||
styledclass={[styles.removeMargin, styles.removePadding]}
|
||||
onClick={(): void => {
|
||||
onToggleHandler(true);
|
||||
setText({
|
||||
subText: value,
|
||||
text: event,
|
||||
});
|
||||
<EllipsedButton
|
||||
{...{
|
||||
event,
|
||||
onToggleHandler,
|
||||
setText,
|
||||
value,
|
||||
}}
|
||||
type="link"
|
||||
>
|
||||
View full log event message
|
||||
</StyledButton>
|
||||
/>
|
||||
)}
|
||||
</CustomSubText>
|
||||
</>
|
||||
@ -71,31 +64,14 @@ function ErrorTag({ event }: ErrorTagProps): JSX.Element {
|
||||
</Collapse>
|
||||
);
|
||||
})}
|
||||
|
||||
<Modal
|
||||
onCancel={(): void => onToggleHandler(false)}
|
||||
title="Log Message"
|
||||
visible={isOpen}
|
||||
destroyOnClose
|
||||
footer={[]}
|
||||
width="70vw"
|
||||
>
|
||||
<CustomSubTitle>{text.text}</CustomSubTitle>
|
||||
|
||||
{text.text === 'exception.stacktrace' ? (
|
||||
<Editor onChange={(): void => {}} readOnly value={text.subText} />
|
||||
) : (
|
||||
<CustomSubText ellipsis={false} isDarkMode={isDarkMode}>
|
||||
{text.subText}
|
||||
</CustomSubText>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ErrorTagProps {
|
||||
event: ITraceTree['event'];
|
||||
onToggleHandler: (isOpen: boolean) => void;
|
||||
setText: (text: { subText: string; text: string }) => void;
|
||||
}
|
||||
|
||||
export default ErrorTag;
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Tabs, Tooltip, Typography } from 'antd';
|
||||
import { Modal, Tabs, Tooltip, Typography } from 'antd';
|
||||
import Editor from 'components/Editor';
|
||||
import { StyledSpace } from 'components/Styled';
|
||||
import useThemeMode from 'hooks/useThemeMode';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
|
||||
import EllipsedButton from './EllipsedButton';
|
||||
import ErrorTag from './ErrorTag';
|
||||
import {
|
||||
CardContainer,
|
||||
@ -12,6 +14,7 @@ import {
|
||||
CustomText,
|
||||
CustomTitle,
|
||||
styles,
|
||||
SubTextContainer,
|
||||
} from './styles';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
@ -26,6 +29,17 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
||||
tree?.serviceName,
|
||||
]);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [text, setText] = useState({
|
||||
text: '',
|
||||
subText: '',
|
||||
});
|
||||
|
||||
const onToggleHandler = (state: boolean): void => {
|
||||
setIsOpen(state);
|
||||
};
|
||||
|
||||
if (!tree) {
|
||||
return <div />;
|
||||
}
|
||||
@ -52,18 +66,60 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
||||
</Tooltip>
|
||||
</StyledSpace>
|
||||
|
||||
<Modal
|
||||
onCancel={(): void => onToggleHandler(false)}
|
||||
title={text.text}
|
||||
visible={isOpen}
|
||||
destroyOnClose
|
||||
footer={[]}
|
||||
width="70vw"
|
||||
centered
|
||||
>
|
||||
{text.text === 'exception.stacktrace' ? (
|
||||
<Editor onChange={(): void => {}} readOnly value={text.subText} />
|
||||
) : (
|
||||
<CustomSubText ellipsis={false} isDarkMode={isDarkMode}>
|
||||
{text.subText}
|
||||
</CustomSubText>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Tabs defaultActiveKey="1">
|
||||
<TabPane tab="Tags" key="1">
|
||||
{tags.length !== 0 ? (
|
||||
tags.map((tags) => {
|
||||
const value = tags.key === 'error' ? 'true' : tags.value;
|
||||
const isEllipsed = value.length > 24;
|
||||
|
||||
return (
|
||||
<React.Fragment key={JSON.stringify(tags)}>
|
||||
{tags.value && (
|
||||
<>
|
||||
<CustomSubTitle>{tags.key}</CustomSubTitle>
|
||||
<CustomSubText isDarkMode={isDarkMode}>
|
||||
{tags.key === 'error' ? 'true' : tags.value}
|
||||
</CustomSubText>
|
||||
<SubTextContainer isDarkMode={isDarkMode}>
|
||||
<Tooltip overlay={(): string => value}>
|
||||
<CustomSubText
|
||||
ellipsis={{
|
||||
rows: isEllipsed ? 1 : 0,
|
||||
}}
|
||||
isDarkMode={isDarkMode}
|
||||
>
|
||||
{value}
|
||||
</CustomSubText>
|
||||
|
||||
{isEllipsed && (
|
||||
<EllipsedButton
|
||||
{...{
|
||||
event: tags.key,
|
||||
onToggleHandler,
|
||||
setText,
|
||||
value,
|
||||
buttonText: 'View full value',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</SubTextContainer>
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
@ -75,7 +131,11 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
||||
</TabPane>
|
||||
<TabPane tab="Events" key="2">
|
||||
{tree.event && Object.keys(tree.event).length !== 0 ? (
|
||||
<ErrorTag event={tree.event} />
|
||||
<ErrorTag
|
||||
onToggleHandler={onToggleHandler}
|
||||
setText={setText}
|
||||
event={tree.event}
|
||||
/>
|
||||
) : (
|
||||
<Typography>No events data in selected span</Typography>
|
||||
)}
|
||||
|
@ -18,7 +18,8 @@ export const CustomText = styled(Paragraph)`
|
||||
export const CustomSubTitle = styled(Title)`
|
||||
&&& {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 0.1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -26,13 +27,19 @@ interface CustomSubTextProps {
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const SubTextContainer = styled.div<CustomSubTextProps>`
|
||||
&&& {
|
||||
background: ${({ isDarkMode }): string => (isDarkMode ? '#444' : '#ddd')};
|
||||
}
|
||||
`;
|
||||
|
||||
export const CustomSubText = styled(Paragraph)<CustomSubTextProps>`
|
||||
&&& {
|
||||
background: ${({ isDarkMode }): string => (isDarkMode ? '#444' : '#ddd')};
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
padding: 4px 8px;
|
||||
word-break: break-all;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -81,10 +88,15 @@ const overflow = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const buttonContainer = css`
|
||||
height: 1.5rem;
|
||||
`;
|
||||
|
||||
export const styles = {
|
||||
removeMargin,
|
||||
removePadding,
|
||||
selectedSpanDetailsContainer,
|
||||
spanEventsTabsContainer,
|
||||
overflow,
|
||||
buttonContainer,
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
const createQueryParams = (params: { [x: string]: string }): string =>
|
||||
const createQueryParams = (params: { [x: string]: string | number }): string =>
|
||||
Object.keys(params)
|
||||
.map((k) => `${k}=${encodeURI(params[k])}`)
|
||||
.map((k) => `${k}=${encodeURI(String(params[k]))}`)
|
||||
.join('&');
|
||||
|
||||
export default createQueryParams;
|
||||
|
@ -13,6 +13,9 @@ const GetMinMax = (
|
||||
if (interval === '1min') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 1 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === '10min') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 10 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === '15min') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 15 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
@ -33,8 +36,9 @@ const GetMinMax = (
|
||||
// one week = one day * 7
|
||||
const minTimeAgo = getMinAgo({ minutes: 26 * 60 * 7 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === '6hr') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 6 * 60 }).getTime();
|
||||
} else if (['4hr', '6hr'].includes(interval)) {
|
||||
const h = parseInt(interval.replace('hr', ''), 10);
|
||||
const minTimeAgo = getMinAgo({ minutes: h * 60 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === 'custom') {
|
||||
maxTime = (dateTimeRange || [])[1] || 0;
|
||||
|
@ -1,109 +1,9 @@
|
||||
import { SaveOutlined } from '@ant-design/icons';
|
||||
import { Button, notification } from 'antd';
|
||||
import createAlertsApi from 'api/alerts/create';
|
||||
import Editor from 'components/Editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { PayloadProps as CreateAlertPayloadProps } from 'types/api/alerts/create';
|
||||
import CreateAlertRule from 'container/CreateAlertRule';
|
||||
import React from 'react';
|
||||
import { alertDefaults } from 'types/api/alerts/create';
|
||||
|
||||
import { ButtonContainer, Title } from './styles';
|
||||
|
||||
function CreateAlert(): JSX.Element {
|
||||
const [value, setEditorValue] = useState<string>(
|
||||
`\n alert: High RPS\n expr: sum(rate(signoz_latency_count{span_kind="SPAN_KIND_SERVER"}[2m])) by (service_name) > 100\n for: 0m\n labels:\n severity: warning\n annotations:\n summary: High RPS of Applications\n description: "RPS is > 100\n\t\t\t VALUE = {{ $value }}\n\t\t\t LABELS = {{ $labels }}"\n `,
|
||||
);
|
||||
|
||||
const [newAlertState, setNewAlertState] = useState<
|
||||
State<CreateAlertPayloadProps>
|
||||
>({
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
payload: undefined,
|
||||
success: false,
|
||||
});
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
|
||||
const defaultError =
|
||||
'Oops! Some issue occured in saving the alert please try again or contact support@signoz.io';
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
try {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
|
||||
if (value.length === 0) {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
}));
|
||||
notifications.error({
|
||||
description: `Oops! We didn't catch that. Please make sure the alert settings are not empty or try again`,
|
||||
message: 'Error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await createAlertsApi({
|
||||
query: value,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Congrats. The alert was saved correctly.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
}, 3000);
|
||||
} else {
|
||||
notifications.error({
|
||||
description: response.error || defaultError,
|
||||
message: 'Error',
|
||||
});
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || defaultError,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: defaultError,
|
||||
});
|
||||
}
|
||||
}, [notifications, value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Title>Create New Alert</Title>
|
||||
<Editor onChange={(value): void => setEditorValue(value)} value={value} />
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={newAlertState.loading || false}
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
);
|
||||
function CreateAlertPage(): JSX.Element {
|
||||
return <CreateAlertRule initialValue={alertDefaults} />;
|
||||
}
|
||||
|
||||
export default CreateAlert;
|
||||
export default CreateAlertPage;
|
||||
|
@ -47,7 +47,12 @@ function EditRules(): JSX.Element {
|
||||
return <Spinner tip="Loading Rules..." />;
|
||||
}
|
||||
|
||||
return <EditRulesContainer ruleId={ruleId} initialData={data.payload.data} />;
|
||||
return (
|
||||
<EditRulesContainer
|
||||
ruleId={parseInt(ruleId, 10)}
|
||||
initialValue={data.payload.data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
@ -4,107 +4,87 @@ import getById from 'api/errors/getById';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ErrorDetailsContainer from 'container/ErrorDetails';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Redirect, useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { PayloadProps } from 'types/api/errors/getById';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { urlKey } from './utils';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function ErrorDetails(): JSX.Element {
|
||||
const { t } = useTranslation(['common']);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const params = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const groupId = params.get(urlKey.groupId);
|
||||
const errorId = params.get(urlKey.errorId);
|
||||
const timestamp = params.get(urlKey.timestamp);
|
||||
|
||||
const errorId = params.get('errorId');
|
||||
const errorType = params.get('errorType');
|
||||
const serviceName = params.get('serviceName');
|
||||
const defaultError = t('something_went_wrong');
|
||||
|
||||
const { data, status } = useQuery(
|
||||
[
|
||||
'errorByType',
|
||||
errorType,
|
||||
'serviceName',
|
||||
serviceName,
|
||||
maxTime,
|
||||
minTime,
|
||||
errorId,
|
||||
],
|
||||
{
|
||||
queryFn: () =>
|
||||
getByErrorType({
|
||||
end: maxTime,
|
||||
errorType: errorType || '',
|
||||
serviceName: serviceName || '',
|
||||
start: minTime,
|
||||
}),
|
||||
enabled: errorId === null && errorType !== null && serviceName !== null,
|
||||
cacheTime: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
const { status: ErrorIdStatus, data: errorIdPayload } = useQuery(
|
||||
[
|
||||
'errorByType',
|
||||
errorType,
|
||||
'serviceName',
|
||||
serviceName,
|
||||
maxTime,
|
||||
minTime,
|
||||
'errorId',
|
||||
errorId,
|
||||
],
|
||||
const { data: IdData, status: IdStatus } = useQuery(
|
||||
[errorId, timestamp, groupId],
|
||||
{
|
||||
queryFn: () =>
|
||||
getById({
|
||||
end: maxTime,
|
||||
errorId: errorId || data?.payload?.errorId || '',
|
||||
start: minTime,
|
||||
errorID: errorId || '',
|
||||
groupID: groupId || '',
|
||||
timestamp: timestamp || '',
|
||||
}),
|
||||
enabled:
|
||||
(errorId !== null || status === 'success') &&
|
||||
errorType !== null &&
|
||||
serviceName !== null,
|
||||
cacheTime: 5000,
|
||||
errorId !== null &&
|
||||
groupId !== null &&
|
||||
timestamp !== null &&
|
||||
errorId.length !== 0 &&
|
||||
groupId.length !== 0 &&
|
||||
timestamp.length !== 0,
|
||||
},
|
||||
);
|
||||
|
||||
const { data, status } = useQuery([maxTime, minTime, groupId], {
|
||||
queryFn: () =>
|
||||
getByErrorType({
|
||||
groupID: groupId || '',
|
||||
timestamp: timestamp || '',
|
||||
}),
|
||||
enabled: !!groupId && IdStatus !== 'success',
|
||||
});
|
||||
|
||||
// if errorType and serviceName is null redirecting to the ALL_ERROR page not now
|
||||
if (errorType === null || serviceName === null) {
|
||||
if (groupId === null || timestamp === null) {
|
||||
return <Redirect to={ROUTES.ALL_ERROR} />;
|
||||
}
|
||||
|
||||
// when the api is in loading state
|
||||
if (status === 'loading' || ErrorIdStatus === 'loading') {
|
||||
if (status === 'loading' || IdStatus === 'loading') {
|
||||
return <Spinner tip="Loading.." />;
|
||||
}
|
||||
|
||||
// if any error occurred while loading
|
||||
if (status === 'error' || ErrorIdStatus === 'error') {
|
||||
return (
|
||||
<Typography>
|
||||
{data?.error || errorIdPayload?.error || defaultError}
|
||||
</Typography>
|
||||
);
|
||||
if (status === 'error' || IdStatus === 'error') {
|
||||
return <Typography>{data?.error || defaultError}</Typography>;
|
||||
}
|
||||
|
||||
const idPayload = data?.payload || IdData?.payload;
|
||||
|
||||
// if API is successfully but there is an error
|
||||
if (
|
||||
(status === 'success' && data?.statusCode >= 400) ||
|
||||
(ErrorIdStatus === 'success' && errorIdPayload.statusCode >= 400)
|
||||
(IdStatus === 'success' && IdData.statusCode >= 400) ||
|
||||
idPayload === null ||
|
||||
idPayload === undefined
|
||||
) {
|
||||
return <Typography>{data?.error || defaultError}</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorDetailsContainer idPayload={errorIdPayload?.payload as PayloadProps} />
|
||||
);
|
||||
return <ErrorDetailsContainer idPayload={idPayload} />;
|
||||
}
|
||||
|
||||
export interface ErrorDetailsParams {
|
||||
|
8
frontend/src/pages/ErrorDetails/utils.ts
Normal file
8
frontend/src/pages/ErrorDetails/utils.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const urlKey = {
|
||||
serviceName: 'serviceName',
|
||||
exceptionType: 'exceptionType',
|
||||
groupId: 'groupId',
|
||||
lastSeen: 'lastSeen',
|
||||
errorId: 'errorId',
|
||||
timestamp: 'timestamp',
|
||||
};
|
@ -262,12 +262,13 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
setState(updateValue, setConfirmPassword);
|
||||
}}
|
||||
required
|
||||
id="UpdatePassword"
|
||||
id="confirmPassword"
|
||||
/>
|
||||
|
||||
{confirmPasswordError && (
|
||||
<Typography.Paragraph
|
||||
italic
|
||||
id="password-confirm-error"
|
||||
style={{
|
||||
color: '#D89614',
|
||||
marginTop: '0.50rem',
|
||||
@ -340,6 +341,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
!organizationName ||
|
||||
!password ||
|
||||
!confirmPassword ||
|
||||
!firstName ||
|
||||
confirmPasswordError ||
|
||||
isPasswordPolicyError
|
||||
}
|
||||
|
@ -64,6 +64,8 @@ const initialValue: TraceReducer = {
|
||||
['httpMethod', INITIAL_FILTER_VALUE],
|
||||
['httpUrl', INITIAL_FILTER_VALUE],
|
||||
['operation', INITIAL_FILTER_VALUE],
|
||||
['rpcMethod', INITIAL_FILTER_VALUE],
|
||||
['responseStatusCode', INITIAL_FILTER_VALUE],
|
||||
['serviceName', INITIAL_FILTER_VALUE],
|
||||
['status', INITIAL_FILTER_VALUE],
|
||||
]),
|
||||
|
64
frontend/src/types/api/alerts/compositeQuery.ts
Normal file
64
frontend/src/types/api/alerts/compositeQuery.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
IMetricsBuilderFormula,
|
||||
IMetricsBuilderQuery,
|
||||
IPromQLQuery,
|
||||
IQueryBuilderTagFilters,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export interface ICompositeMetricQuery {
|
||||
builderQueries: IBuilderQueries;
|
||||
promQueries: IPromQueries;
|
||||
queryType: EQueryType;
|
||||
}
|
||||
|
||||
export interface IPromQueries {
|
||||
[key: string]: IPromQuery;
|
||||
}
|
||||
|
||||
export interface IPromQuery extends IPromQLQuery {
|
||||
stats?: '';
|
||||
}
|
||||
|
||||
export interface IBuilderQueries {
|
||||
[key: string]: IBuilderQuery;
|
||||
}
|
||||
|
||||
// IBuilderQuery combines IMetricQuery and IFormulaQuery
|
||||
// for api calls
|
||||
export interface IBuilderQuery
|
||||
extends Omit<
|
||||
IMetricQuery,
|
||||
'aggregateOperator' | 'legend' | 'metricName' | 'tagFilters'
|
||||
>,
|
||||
Omit<IFormulaQuery, 'expression'> {
|
||||
aggregateOperator: EAggregateOperator | undefined;
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
legend?: string;
|
||||
metricName: string | null;
|
||||
groupBy?: string[];
|
||||
expression?: string;
|
||||
tagFilters?: IQueryBuilderTagFilters;
|
||||
toggleDisable?: boolean;
|
||||
toggleDelete?: boolean;
|
||||
}
|
||||
|
||||
export interface IFormulaQueries {
|
||||
[key: string]: IFormulaQuery;
|
||||
}
|
||||
|
||||
export interface IFormulaQuery extends IMetricsBuilderFormula {
|
||||
formulaOnly: boolean;
|
||||
queryName: string;
|
||||
}
|
||||
|
||||
export interface IMetricQueries {
|
||||
[key: string]: IMetricQuery;
|
||||
}
|
||||
|
||||
export interface IMetricQuery extends IMetricsBuilderQuery {
|
||||
formulaOnly: boolean;
|
||||
expression?: string;
|
||||
queryName: string;
|
||||
}
|
@ -1,8 +1,48 @@
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { defaultCompareOp, defaultEvalWindow, defaultMatchType } from './def';
|
||||
|
||||
export interface Props {
|
||||
query: string;
|
||||
data: AlertDef;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export const alertDefaults: AlertDef = {
|
||||
condition: {
|
||||
compositeMetricQuery: {
|
||||
builderQueries: {
|
||||
A: {
|
||||
queryName: 'A',
|
||||
name: 'A',
|
||||
formulaOnly: false,
|
||||
metricName: '',
|
||||
tagFilters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
groupBy: [],
|
||||
aggregateOperator: 1,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
toggleDisable: false,
|
||||
toggleDelete: false,
|
||||
},
|
||||
},
|
||||
promQueries: {},
|
||||
queryType: 1,
|
||||
},
|
||||
op: defaultCompareOp,
|
||||
matchType: defaultMatchType,
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
annotations: {
|
||||
description: 'A new alert',
|
||||
},
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
32
frontend/src/types/api/alerts/def.ts
Normal file
32
frontend/src/types/api/alerts/def.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
|
||||
// default match type for threshold
|
||||
export const defaultMatchType = '1';
|
||||
|
||||
// default eval window
|
||||
export const defaultEvalWindow = '5m0s';
|
||||
|
||||
// default compare op: above
|
||||
export const defaultCompareOp = '1';
|
||||
|
||||
export interface AlertDef {
|
||||
id?: number;
|
||||
alert?: string;
|
||||
ruleType?: string;
|
||||
condition: RuleCondition;
|
||||
labels?: Labels;
|
||||
annotations?: Labels;
|
||||
evalWindow?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface RuleCondition {
|
||||
compositeMetricQuery: ICompositeMetricQuery;
|
||||
op?: string | undefined;
|
||||
target?: number | undefined;
|
||||
matchType?: string | undefined;
|
||||
}
|
||||
|
||||
export interface Labels {
|
||||
[key: string]: string;
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { Alerts } from './getAll';
|
||||
import { AlertDef } from './def';
|
||||
|
||||
export interface Props {
|
||||
id: Alerts['id'];
|
||||
id: AlertDef['id'];
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
data: string;
|
||||
data: AlertDef;
|
||||
};
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { PayloadProps as DeletePayloadProps } from './delete';
|
||||
import { Alerts } from './getAll';
|
||||
|
||||
export type PayloadProps = DeletePayloadProps;
|
||||
|
||||
export interface Props {
|
||||
id: Alerts['id'];
|
||||
data: DeletePayloadProps['data'];
|
||||
}
|
17
frontend/src/types/api/alerts/queryType.ts
Normal file
17
frontend/src/types/api/alerts/queryType.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export type QueryType = 1 | 2 | 3;
|
||||
|
||||
export const QUERY_BUILDER: QueryType = 1;
|
||||
export const PROMQL: QueryType = 3;
|
||||
|
||||
export const resolveQueryCategoryName = (s: number): string => {
|
||||
switch (s) {
|
||||
case 1:
|
||||
return 'Query Builder';
|
||||
case 2:
|
||||
return 'Clickhouse Query';
|
||||
case 3:
|
||||
return 'PromQL';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
11
frontend/src/types/api/alerts/save.ts
Normal file
11
frontend/src/types/api/alerts/save.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { AlertDef } from './def';
|
||||
|
||||
export type PayloadProps = {
|
||||
status: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
id?: number;
|
||||
data: AlertDef;
|
||||
}
|
@ -1,8 +1,20 @@
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
|
||||
export type Order = 'ascending' | 'descending';
|
||||
export type OrderBy =
|
||||
| 'serviceName'
|
||||
| 'exceptionCount'
|
||||
| 'lastSeen'
|
||||
| 'firstSeen'
|
||||
| 'exceptionType';
|
||||
|
||||
export interface Props {
|
||||
start: GlobalTime['minTime'];
|
||||
end: GlobalTime['maxTime'];
|
||||
order?: Order;
|
||||
orderParam?: OrderBy;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface Exception {
|
||||
@ -12,6 +24,7 @@ export interface Exception {
|
||||
lastSeen: string;
|
||||
firstSeen: string;
|
||||
serviceName: string;
|
||||
groupID: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = Exception[];
|
||||
|
9
frontend/src/types/api/errors/getByErrorId.ts
Normal file
9
frontend/src/types/api/errors/getByErrorId.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { PayloadProps as Prop } from './getByErrorTypeAndService';
|
||||
|
||||
export interface Props {
|
||||
groupID: string;
|
||||
errorID: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = Prop;
|
@ -1,10 +1,6 @@
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
|
||||
export interface Props {
|
||||
start: GlobalTime['minTime'];
|
||||
end: GlobalTime['maxTime'];
|
||||
serviceName: string;
|
||||
errorType: string;
|
||||
timestamp: string;
|
||||
groupID: string;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
@ -16,7 +12,6 @@ export interface PayloadProps {
|
||||
timestamp: string;
|
||||
spanID: string;
|
||||
traceID: string;
|
||||
serviceName: Props['serviceName'];
|
||||
newerErrorId: string;
|
||||
olderErrorId: string;
|
||||
serviceName: string;
|
||||
groupID: string;
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
|
||||
import { PayloadProps as Payload } from './getByErrorTypeAndService';
|
||||
|
||||
export type PayloadProps = Payload;
|
||||
|
||||
export type Props = {
|
||||
start: GlobalTime['minTime'];
|
||||
end: GlobalTime['minTime'];
|
||||
errorId: string;
|
||||
};
|
||||
|
||||
export type PayloadProps = number;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user