Merge branch 'SigNoz:develop' into develop

This commit is contained in:
Priyansh Khodiyar 2022-07-15 13:38:54 +05:30 committed by GitHub
commit 65804f245c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
145 changed files with 9164 additions and 1420 deletions

22
.github/workflows/dependency-review.yml vendored Normal file
View 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

View File

@ -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/*"

View File

@ -86,15 +86,19 @@ services:
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
@ -111,6 +115,11 @@ services:
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

View File

@ -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]

View File

@ -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]

View File

@ -82,16 +82,20 @@ services:
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:
@ -103,6 +107,11 @@ services:
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:

View File

@ -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]

View File

@ -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]

View File

@ -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>

View File

@ -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",

View File

@ -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;

View File

@ -0,0 +1,63 @@
{
"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"
}

View File

@ -0,0 +1,63 @@
{
"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"
}

View File

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

View File

@ -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);

View File

@ -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 {

View 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;

View File

@ -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,
})}`,
);

View File

@ -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,
})}`,
);

View File

@ -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,
})}`,
);

View 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;

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,3 @@
const getVersion = 'version';
export { getVersion };

View File

@ -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
}&timestamp=${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}
/>
);
}

View 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');
});
});

View 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): number => {
return (
parseInt((new Date(date).getTime() / 1e3).toString(), 10) * 1e9 +
Timestamp.fromString(date).getNano()
);
};
export const getUpdatePageSize = (pageSize: string | null): number => {
if (pageSize) {
return parseInt(pageSize, 10);
}
return 10;
};

View 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;

View File

@ -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;

View File

@ -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).toString(),
}),
},
);
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,
timespamp: 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
}&timestamp=${getNanoSeconds(timespamp)}&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;

View 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('rules');
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;

View 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('rules');
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;

View 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; */
}
`;

View 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;

View 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('rules');
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;

View 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('rules');
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;

View File

@ -0,0 +1,366 @@
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 } from './styles';
import useDebounce from './useDebounce';
import {
prepareBuilderQueries,
prepareStagedQuery,
toChartInterval,
toFormulaQueries,
toMetricQueries,
} from './utils';
function FormAlertRules({
formInstance,
initialValue,
ruleId,
}: FormAlertRuleProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('rules');
// 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}
<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>
</>
);
}
interface FormAlertRuleProps {
formInstance: FormInstance;
initialValue: AlertDef;
ruleId: number;
}
export default FormAlertRules;

View File

@ -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',
});

View File

@ -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;
}

View 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>
);
}

View 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('rules');
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;

View 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;
`;

View File

@ -0,0 +1,9 @@
export interface ILabelRecord {
key: string;
value: string;
}
export interface IOption {
label: string;
value: string;
}

View 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;
};

View File

@ -0,0 +1,90 @@
import { Button, Card, Form, Input, InputNumber, Select } from 'antd';
import TextArea from 'antd/lib/input/TextArea';
import styled from 'styled-components';
export const MainFormContainer = styled(Form)`
max-width: 900px;
`;
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.div`
padding: 2em;
margin-top: 1rem;
display: flex;
flex-direction: column;
background: #141414;
border-radius: 4px;
border: 1px solid #303030;
`;
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%;
`;

View 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;
}

View 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';
}
};

View File

@ -10,7 +10,7 @@ function SpanNameComponent({
<Container title={`${name} ${serviceName}`}>
<SpanWrapper>
<Span ellipsis>{name}</Span>
<Service>{serviceName}</Service>
<Service ellipsis>{serviceName}</Service>
</SpanWrapper>
</Container>
);

View File

@ -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;

View File

@ -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 {

View File

@ -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;

View File

@ -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>
);
})}

View File

@ -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}}',
},
])}

View File

@ -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,
},
])}

View File

@ -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',
},
])}

View File

@ -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,

View File

@ -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 } });
};

View File

@ -7,7 +7,7 @@ import { IPromQLQueryHandleChange } from './types';
interface IPromQLQueryBuilderProps {
queryData: IPromQLQuery;
queryIndex: number;
queryIndex: number | string;
handleQueryChange: (args: IPromQLQueryHandleChange) => void;
}

View File

@ -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'];

View File

@ -9,7 +9,7 @@ const { TextArea } = Input;
interface IMetricsBuilderFormulaProps {
formulaData: IMetricsBuilderFormula;
formulaIndex: number;
formulaIndex: number | string;
handleFormulaChange: (args: IQueryBuilderFormulaHandleChange) => void;
}
function MetricsBuilderFormula({

View File

@ -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 } });
};

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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 {

View File

@ -9,7 +9,9 @@ export const AllTraceFilterEnum: TraceFilterEnum[] = [
'serviceName',
'operation',
'component',
'httpCode',
'rpcMethod',
'responseStatusCode',
// 'httpCode',
'httpHost',
'httpMethod',
'httpRoute',

View File

@ -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',

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 {

View File

@ -0,0 +1,8 @@
export const urlKey = {
serviceName: 'serviceName',
exceptionType: 'exceptionType',
groupId: 'groupId',
lastSeen: 'lastSeen',
errorId: 'errorId',
timestamp: 'timestamp',
};

View File

@ -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
}

View File

@ -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],
]),

View 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;
}

View File

@ -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,
};

View 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;
}

View File

@ -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;
};

View File

@ -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'];
}

View 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 '';
}
};

View File

@ -0,0 +1,11 @@
import { AlertDef } from './def';
export type PayloadProps = {
status: string;
data: string;
};
export interface Props {
id?: number;
data: AlertDef;
}

View File

@ -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[];

View File

@ -0,0 +1,9 @@
import { PayloadProps as Prop } from './getByErrorTypeAndService';
export interface Props {
groupID: string;
errorID: string;
timestamp: string;
}
export type PayloadProps = Prop;

View File

@ -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;
}

View File

@ -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;

View File

@ -0,0 +1,13 @@
export type Props = {
errorID: string;
timestamp: string;
groupID: string;
};
export type PayloadProps = {
prevErrorID: string;
nextErrorID: string;
groupID: string;
nextTimestamp: string;
prevTimestamp: string;
};

View File

@ -18,10 +18,10 @@ export type Span = [
string,
string,
string,
string | string[],
string | string[],
string | string[],
Record<string, unknown>[],
string[],
string[],
string[],
string[],
boolean,
];

View File

@ -69,7 +69,9 @@ export type TraceFilterEnum =
| 'httpUrl'
| 'operation'
| 'serviceName'
| 'status';
| 'status'
| 'responseStatusCode'
| 'rpcMethod';
export const AllPanelHeading: {
key: TraceFilterEnum;
@ -107,6 +109,14 @@ export const AllPanelHeading: {
key: 'operation',
displayValue: 'Operation',
},
{
key: 'responseStatusCode',
displayValue: 'Status Code',
},
{
key: 'rpcMethod',
displayValue: 'RPC Method',
},
{
key: 'serviceName',
displayValue: 'Service Name',

View File

@ -0,0 +1,211 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`utils/spanToTree should return a single tree on valid trace data 1`] = `
Object {
"missingSpanTree": Array [],
"spanTree": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"children": Array [],
"event": Array [
Object {
"attributeMap": Object {
"event": "HTTP request received S3",
"level": "info",
"method": "GET",
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
},
"timeUnixNano": 1657275433246142000,
},
],
"hasError": false,
"id": "span_3",
"isProcessed": true,
"name": "HTTP GET SPAN 3",
"references": Array [
Object {
"RefType": "CHILD_OF",
"SpanId": "span_2",
"TraceId": "0000000000000000span_1",
},
],
"serviceColour": "",
"serviceName": "frontend",
"startTime": 1657275433246,
"tags": Array [
Object {
"key": "host.name.span3",
"value": "span_3",
},
],
"time": 683273000,
"value": 683273000,
},
],
"event": Array [
Object {
"attributeMap": Object {
"event": "HTTP request received S2",
"level": "info",
"method": "GET",
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
},
"timeUnixNano": 1657275433246142000,
},
],
"hasError": false,
"id": "span_2",
"isProcessed": true,
"name": "HTTP GET SPAN 2",
"references": Array [
Object {
"RefType": "CHILD_OF",
"SpanId": "span_1",
"TraceId": "0000000000000000span_1",
},
],
"serviceColour": "",
"serviceName": "frontend",
"startTime": 1657275433246,
"tags": Array [
Object {
"key": "host.name.span2",
"value": "span_2",
},
],
"time": 683273000,
"value": 683273000,
},
],
"event": Array [
Object {
"attributeMap": Object {
"event": "HTTP request received S1",
"level": "info",
"method": "GET",
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
},
"timeUnixNano": 1657275433246142000,
},
],
"hasError": false,
"id": "span_1",
"name": "HTTP GET SPAN 1",
"references": Array [
Object {
"RefType": "CHILD_OF",
"SpanId": "",
"TraceId": "0000000000000000span_1",
},
],
"serviceColour": "",
"serviceName": "frontend",
"startTime": 1657275433246,
"tags": Array [
Object {
"key": "host.name.span1",
"value": "span_1",
},
],
"time": 683273000,
"value": 683273000,
},
],
}
`;
exports[`utils/spanToTree should return a single tree on valid trace data 2`] = `
Object {
"missingSpanTree": Array [
Object {
"children": Array [
Object {
"children": Array [],
"event": Array [
Object {
"attributeMap": Object {
"event": "HTTP request received S3",
"level": "info",
"method": "GET",
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
},
"timeUnixNano": 1657275433246142000,
},
],
"hasError": false,
"id": "span_3",
"isProcessed": true,
"name": "HTTP GET SPAN 3",
"references": Array [
Object {
"RefType": "CHILD_OF",
"SpanId": "span_2",
"TraceId": "0000000000000000span_1",
},
],
"serviceColour": "",
"serviceName": "frontend",
"startTime": 1657275433246,
"tags": Array [
Object {
"key": "host.name.span3",
"value": "span_3",
},
],
"time": 683273000,
"value": 683273000,
},
],
"id": "span_2",
"isMissing": true,
"name": "Missing Span (span_2)",
"serviceColour": "",
"serviceName": "",
"startTime": null,
"tags": Array [],
"time": null,
"value": null,
},
],
"spanTree": Array [
Object {
"children": Array [],
"event": Array [
Object {
"attributeMap": Object {
"event": "HTTP request received S1",
"level": "info",
"method": "GET",
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
},
"timeUnixNano": 1657275433246142000,
},
],
"hasError": false,
"id": "span_1",
"name": "HTTP GET SPAN 1",
"references": Array [
Object {
"RefType": "CHILD_OF",
"SpanId": "",
"TraceId": "0000000000000000span_1",
},
],
"serviceColour": "",
"serviceName": "frontend",
"startTime": 1657275433246,
"tags": Array [
Object {
"key": "host.name.span1",
"value": "span_1",
},
],
"time": 683273000,
"value": 683273000,
},
],
}
`;

View File

@ -0,0 +1,21 @@
import { TraceData } from '../fixtures/TraceData';
import { spanToTreeUtil } from '../spanToTree';
describe('utils/spanToTree', () => {
test('should return a single tree on valid trace data', () => {
const spanTree = spanToTreeUtil(TraceData);
expect(spanTree.spanTree.length).toBe(1);
expect(spanTree.missingSpanTree.length).toBe(0);
expect(spanTree).toMatchSnapshot();
});
test('should return a single tree on valid trace data', () => {
const MissingTraceData = [...TraceData];
MissingTraceData.splice(1, 1);
const spanTree = spanToTreeUtil(MissingTraceData);
expect(spanTree.spanTree.length).toBe(1);
expect(spanTree.missingSpanTree.length).toBe(1);
expect(spanTree).toMatchSnapshot();
});
});

View File

@ -0,0 +1,52 @@
import { Span } from 'types/api/trace/getTraceItem';
export const TraceData: Span[] = [
[
1657275433246,
'span_1',
'0000000000000000span_1',
'frontend',
'HTTP GET SPAN 1',
'2',
'683273000',
['host.name.span1'],
['span_1'],
['{TraceId=0000000000000000span_1, SpanId=, RefType=CHILD_OF}'],
[
'{"timeUnixNano":1657275433246142000,"attributeMap":{"event":"HTTP request received S1","level":"info","method":"GET","url":"/dispatch?customer=392\\u0026nonse=0.015296363321630757"}}',
],
false,
],
[
1657275433246,
'span_2',
'0000000000000000span_1',
'frontend',
'HTTP GET SPAN 2',
'2',
'683273000',
['host.name.span2'],
['span_2'],
['{TraceId=0000000000000000span_1, SpanId=span_1, RefType=CHILD_OF}'],
[
'{"timeUnixNano":1657275433246142000,"attributeMap":{"event":"HTTP request received S2","level":"info","method":"GET","url":"/dispatch?customer=392\\u0026nonse=0.015296363321630757"}}',
],
false,
],
[
1657275433246,
'span_3',
'0000000000000000span_1',
'frontend',
'HTTP GET SPAN 3',
'2',
'683273000',
['host.name.span3'],
['span_3'],
['{TraceId=0000000000000000span_1, SpanId=span_2, RefType=CHILD_OF}'],
[
'{"timeUnixNano":1657275433246142000,"attributeMap":{"event":"HTTP request received S3","level":"info","method":"GET","url":"/dispatch?customer=392\\u0026nonse=0.015296363321630757"}}',
],
false,
],
];

View File

@ -109,6 +109,12 @@ export const spanToTreeUtil = (inputSpanList: Span[]): ITraceForest => {
const missingSpanTree: ITraceTree[] = [];
const referencedTraceIds: string[] = Array.from(traceIdSet);
Object.keys(spanMap).forEach((spanId) => {
const isRoot = spanMap[spanId].references?.some((refs) => refs.SpanId === '');
if (isRoot) {
spanTree.push(spanMap[spanId]);
return;
}
for (const traceId of referencedTraceIds) {
if (traceId.includes(spanId)) {
spanTree.push(spanMap[spanId]);

38
frontend/tests/auth.json Normal file
View File

@ -0,0 +1,38 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:3301",
"localStorage": [
{
"name": "isSideBarCollapsed",
"value": "false"
},
{
"name": "metricsTimeDurations",
"value": "{}"
},
{
"name": "i18nextLng",
"value": "en-US"
},
{
"name": "reactQueryDevtoolsSortFn",
"value": "\"Status > Last Updated\""
},
{
"name": "AUTH_TOKEN",
"value": "authtoken"
},
{
"name": "IS_LOGGED_IN",
"value": "true"
},
{
"name": "REFRESH_AUTH_TOKEN",
"value": "refreshJwt"
}
]
}
]
}

View File

@ -0,0 +1,7 @@
{
"accessJwt": "authtoken",
"accessJwtExpiry": 1656609177,
"refreshJwt": "refreshJwt",
"refreshJwtExpiry": 1659199377,
"userId": "34917776-514b-4b95-a4f5-1a5cc06e34b6"
}

View File

@ -0,0 +1,3 @@
{
"data": "org updated successfully"
}

View File

@ -0,0 +1 @@
{ "data": "user registered successfully" }

View File

@ -0,0 +1,5 @@
{
"status": "error",
"errorType": "unauthorized",
"error": "You are not allowed to create an account. Please ask your admin to send an invite link"
}

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