Merge pull request #1406 from SigNoz/release/v0.10.0

Release/v0.10.0
This commit is contained in:
Ankit Nayan 2022-07-15 22:19:13 +05:30 committed by GitHub
commit 73e699080d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
171 changed files with 10213 additions and 3046 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

@ -22,7 +22,7 @@
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
-->
<level>trace</level>
<level>information</level>
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
<!-- Rotation policy

View File

@ -40,7 +40,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.9.2
image: signoz/query-service:0.10.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@ -68,7 +68,7 @@ services:
- clickhouse
frontend:
image: signoz/frontend:0.9.2
image: signoz/frontend:0.10.0
deploy:
restart_policy:
condition: on-failure
@ -81,20 +81,24 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.45.1-1.0
image: signoz/otelcontribcol:0.45.1-1.1
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8889:8889" # Prometheus metrics exposed by the agent
# - "13133:13133" # health_check
# - "14268:14268" # Jaeger receiver
# - "8888:8888" # OtelCollector internal metrics
# - "8889:8889" # signoz spanmetrics exposed by the agent
# - "9411:9411" # Zipkin port
# - "13133:13133" # Health check extension
# - "14250:14250" # Jaeger gRPC
# - "14268:14268" # Jaeger thrift HTTP
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
# - "55680:55680" # OTLP gRPC legacy receiver
# - "55681:55681" # OTLP HTTP legacy receiver
# - "55679:55679" # zPages extension
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
deploy:
mode: replicated
replicas: 3
@ -107,10 +111,15 @@ services:
- clickhouse
otel-collector-metrics:
image: signoz/otelcontribcol:0.45.1-1.0
image: signoz/otelcontribcol:0.45.1-1.1
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
# ports:
# - "1777:1777" # pprof extension
# - "8888:8888" # OtelCollector internal metrics
# - "13133:13133" # Health check extension
# - "55679:55679" # zPages extension
deploy:
restart_policy:
condition: on-failure

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

@ -22,7 +22,7 @@
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
-->
<level>trace</level>
<level>information</level>
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
<!-- Rotation policy

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:0.9.2
image: signoz/query-service:0.10.0
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@ -66,7 +66,7 @@ services:
condition: service_healthy
frontend:
image: signoz/frontend:0.9.2
image: signoz/frontend:0.10.0
container_name: frontend
restart: on-failure
depends_on:
@ -78,20 +78,24 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.45.1-1.0
image: signoz/otelcontribcol:0.45.1-1.1
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8889:8889" # Prometheus metrics exposed by the agent
# - "13133:13133" # health_check
# - "14268:14268" # Jaeger receiver
# - "8888:8888" # OtelCollector internal metrics
# - "8889:8889" # signoz spanmetrics exposed by the agent
# - "9411:9411" # Zipkin port
# - "13133:13133" # health check extension
# - "14250:14250" # Jaeger gRPC
# - "14268:14268" # Jaeger thrift HTTP
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
# - "55680:55680" # OTLP gRPC legacy receiver
# - "55681:55681" # OTLP HTTP legacy receiver
# - "55679:55679" # zPages extension
mem_limit: 2000m
restart: on-failure
depends_on:
@ -99,10 +103,15 @@ services:
condition: service_healthy
otel-collector-metrics:
image: signoz/otelcontribcol:0.45.1-1.0
image: signoz/otelcontribcol:0.45.1-1.1
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
# ports:
# - "1777:1777" # pprof extension
# - "8888:8888" # OtelCollector internal metrics
# - "13133:13133" # Health check extension
# - "55679:55679" # zPages extension
restart: on-failure
depends_on:
clickhouse:

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,85 @@
{
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
"button_yes": "Yes",
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
"rule_created": "Rule created successfully",
"rule_edited": "Rule edited successfully",
"expression_missing": "expression is missing in {{where}}",
"metricname_missing": "metric name is missing in {{where}}",
"condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL",
"button_savechanges": "Save Rule",
"button_createrule": "Create Rule",
"button_returntorules": "Return to rules",
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when the metric is",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_60min": "60 mins",
"option_4hours": "4 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
"button_formula": "Formula",
"tab_qb": "Query Builder",
"tab_promql": "PromQL",
"title_confirm": "Confirm",
"button_ok": "Yes",
"button_cancel": "No",
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",
"option_error": "Error",
"option_warning": "Warning",
"option_info": "Info",
"user_guide_headline": "Steps to create an Alert",
"user_guide_qb_step1": "Step 1 - Define the metric",
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
"user_guide_qb_step2b": "Enter the Alert threshold",
"user_guide_qb_step3": "Step 3 -Alert Configuration",
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
"user_guide_pql_step1": "Step 1 - Define the metric",
"user_guide_pql_step1a": "Write a PromQL query for the metric",
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_pql_step2b": "Enter the Alert threshold",
"user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts"
}

View File

@ -0,0 +1,85 @@
{
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
"button_yes": "Yes",
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
"rule_created": "Rule created successfully",
"rule_edited": "Rule edited successfully",
"expression_missing": "expression is missing in {{where}}",
"metricname_missing": "metric name is missing in {{where}}",
"condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL",
"button_savechanges": "Save Rule",
"button_createrule": "Create Rule",
"button_returntorules": "Return to rules",
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when the metric is",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_60min": "60 mins",
"option_4hours": "4 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
"button_formula": "Formula",
"tab_qb": "Query Builder",
"tab_promql": "PromQL",
"title_confirm": "Confirm",
"button_ok": "Yes",
"button_cancel": "No",
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",
"option_error": "Error",
"option_warning": "Warning",
"option_info": "Info",
"user_guide_headline": "Steps to create an Alert",
"user_guide_qb_step1": "Step 1 - Define the metric",
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
"user_guide_qb_step2b": "Enter the Alert threshold",
"user_guide_qb_step3": "Step 3 -Alert Configuration",
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
"user_guide_pql_step1": "Step 1 - Define the metric",
"user_guide_pql_step1a": "Write a PromQL query for the metric",
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_pql_step2b": "Enter the Alert threshold",
"user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts"
}

View File

@ -0,0 +1,85 @@
{
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
"button_yes": "Yes",
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
"rule_created": "Rule created successfully",
"rule_edited": "Rule edited successfully",
"expression_missing": "expression is missing in {{where}}",
"metricname_missing": "metric name is missing in {{where}}",
"condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL",
"button_savechanges": "Save Rule",
"button_createrule": "Create Rule",
"button_returntorules": "Return to rules",
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when the metric is",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_60min": "60 mins",
"option_4hours": "4 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
"button_formula": "Formula",
"tab_qb": "Query Builder",
"tab_promql": "PromQL",
"title_confirm": "Confirm",
"button_ok": "Yes",
"button_cancel": "No",
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",
"option_error": "Error",
"option_warning": "Warning",
"option_info": "Info",
"user_guide_headline": "Steps to create an Alert",
"user_guide_qb_step1": "Step 1 - Define the metric",
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
"user_guide_qb_step2b": "Enter the Alert threshold",
"user_guide_qb_step3": "Step 3 -Alert Configuration",
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
"user_guide_pql_step1": "Step 1 - Define the metric",
"user_guide_pql_step1a": "Write a PromQL query for the metric",
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_pql_step2b": "Enter the Alert threshold",
"user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts"
}

View File

@ -0,0 +1,85 @@
{
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
"button_yes": "Yes",
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
"rule_created": "Rule created successfully",
"rule_edited": "Rule edited successfully",
"expression_missing": "expression is missing in {{where}}",
"metricname_missing": "metric name is missing in {{where}}",
"condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL",
"button_savechanges": "Save Rule",
"button_createrule": "Create Rule",
"button_returntorules": "Return to rules",
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when the metric is",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_60min": "60 mins",
"option_4hours": "4 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
"button_formula": "Formula",
"tab_qb": "Query Builder",
"tab_promql": "PromQL",
"title_confirm": "Confirm",
"button_ok": "Yes",
"button_cancel": "No",
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",
"option_error": "Error",
"option_warning": "Warning",
"option_info": "Info",
"user_guide_headline": "Steps to create an Alert",
"user_guide_qb_step1": "Step 1 - Define the metric",
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
"user_guide_qb_step2b": "Enter the Alert threshold",
"user_guide_qb_step3": "Step 3 -Alert Configuration",
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
"user_guide_pql_step1": "Step 1 - Define the metric",
"user_guide_pql_step1a": "Write a PromQL query for the metric",
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_pql_step2b": "Enter the Alert threshold",
"user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts"
}

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

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),
}),
},
);
const errorDetail = idPayload;
@ -48,34 +72,32 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
'errorId',
'timestamp',
'exceptionMessage',
'newerErrorId',
'olderErrorId',
'exceptionEscaped',
],
[],
);
const onClickErrorIdHandler = async (id: string): Promise<void> => {
const onClickErrorIdHandler = async (
id: string,
timestamp: string,
): Promise<void> => {
try {
setLoading(true);
if (id.length === 0) {
notification.error({
message: 'Error Id cannot be empty',
});
setLoading(false);
return;
}
setLoading(false);
history.push(
`${history.location.pathname}?errorId=${id}&serviceName=${serviceName}&errorType=${errorType}`,
history.replace(
`${history.location.pathname}?&groupId=${
idPayload.groupID
}&timestamp=${getNanoSeconds(timestamp)}&errorId=${id}`,
);
} catch (error) {
notification.error({
message: t('something_went_wrong'),
});
setLoading(false);
}
};
@ -106,25 +128,25 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<div>
<Space align="end" direction="horizontal">
<Button
loading={isLoading}
disabled={
errorDetail.olderErrorId.length === 0 ||
queryErrorId === errorDetail.olderErrorId
}
loading={nextPrevStatus === 'loading'}
disabled={nextPrevData?.payload?.prevErrorID.length === 0}
onClick={(): Promise<void> =>
onClickErrorIdHandler(errorDetail.olderErrorId)
onClickErrorIdHandler(
nextPrevData?.payload?.prevErrorID || '',
nextPrevData?.payload?.prevTimestamp || '',
)
}
>
{t('older')}
</Button>
<Button
loading={isLoading}
disabled={
errorDetail.newerErrorId.length === 0 ||
queryErrorId === errorDetail.newerErrorId
}
loading={nextPrevStatus === 'loading'}
disabled={nextPrevData?.payload?.nextErrorID.length === 0}
onClick={(): Promise<void> =>
onClickErrorIdHandler(errorDetail.newerErrorId)
onClickErrorIdHandler(
nextPrevData?.payload?.nextErrorID || '',
nextPrevData?.payload?.nextTimestamp || '',
)
}
>
{t('newer')}
@ -153,7 +175,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
}
interface ErrorDetailsProps {
idPayload: PayloadProps;
idPayload: GetByErrorTypeAndServicePayload;
}
export default ErrorDetails;

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('alerts');
return (
<>
<StepHeading> {t('alert_form_step3')} </StepHeading>
<FormContainer>
<FormItem
label={t('field_severity')}
labelAlign="left"
name={['labels', 'severity']}
>
<SeveritySelect
defaultValue="critical"
onChange={(value: unknown | string): void => {
const s = (value as string) || 'critical';
setAlertDef({
...alertDef,
labels: {
...alertDef.labels,
severity: s,
},
});
}}
>
<Option value="critical">{t('option_critical')}</Option>
<Option value="error">{t('option_error')}</Option>
<Option value="warning">{t('option_warning')}</Option>
<Option value="info">{t('option_info')}</Option>
</SeveritySelect>
</FormItem>
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
<InputSmall
onChange={(e): void => {
setAlertDef({
...alertDef,
alert: e.target.value,
});
}}
/>
</FormItem>
<FormItem
label={t('field_alert_desc')}
labelAlign="left"
name={['annotations', 'description']}
>
<TextareaMedium
onChange={(e): void => {
setAlertDef({
...alertDef,
annotations: {
...alertDef.annotations,
description: e.target.value,
},
});
}}
/>
</FormItem>
<FormItem label={t('field_labels')}>
<LabelSelect
onSetLabels={(l: Labels): void => {
setAlertDef({
...alertDef,
labels: {
...l,
},
});
}}
initialValues={alertDef.labels}
/>
</FormItem>
</FormContainer>
</>
);
}
export default BasicInfo;

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('alerts');
const staticLine: StaticLineProps | undefined =
threshold && threshold > 0
? {
yMin: threshold,
yMax: threshold,
borderColor: '#f14',
borderWidth: 1,
lineText: `${t('preview_chart_threshold_label')} (y=${threshold})`,
textColor: '#f14',
}
: undefined;
const queryKey = JSON.stringify(query);
const queryResponse = useQuery({
queryKey: ['chartPreview', queryKey, selectedInterval],
queryFn: () =>
GetMetricQueryRange({
query: query || {
queryType: 1,
promQL: [],
metricsBuilder: {
formulas: [],
queryBuilder: [],
},
clickHouse: [],
},
globalSelectedInterval: selectedInterval,
graphType,
selectedTime,
}),
enabled:
query != null &&
(query.queryType !== EQueryType.PROM ||
(query.promQL?.length > 0 && query.promQL[0].query !== '')),
});
const chartDataSet = queryResponse.isError
? null
: getChartData({
queryData: [
{
queryData: queryResponse?.data?.payload?.data?.result
? queryResponse?.data?.payload?.data?.result
: [],
},
],
});
return (
<ChartContainer>
{headline}
{(queryResponse?.data?.error || queryResponse?.isError) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '}
{queryResponse?.data?.error ||
queryResponse?.error ||
t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{chartDataSet && !queryResponse.isError && (
<GridGraphComponent
title={name}
data={chartDataSet}
isStacked
GRAPH_TYPES={graphType || 'TIME_SERIES'}
name={name || 'Chart Preview'}
staticLine={staticLine}
/>
)}
</ChartContainer>
);
}
ChartPreview.defaultProps = {
graphType: 'TIME_SERIES',
selectedTime: 'GLOBAL_TIME',
selectedInterval: '5min',
headline: undefined,
threshold: 0,
};
export default ChartPreview;

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('alerts');
const handleQueryCategoryChange = (s: string): void => {
if (
parseInt(s, 10) === EQueryType.PROM &&
(!promQueries || Object.keys(promQueries).length === 0)
) {
setPromQueries({
A: {
query: '',
stats: '',
name: 'A',
legend: '',
disabled: false,
},
});
}
setQueryCategory(parseInt(s, 10));
};
const getNextQueryLabel = useCallback((): string => {
let maxAscii = 0;
Object.keys(metricQueries).forEach((key) => {
const n = key.charCodeAt(0);
if (n > maxAscii) {
maxAscii = n - 64;
}
});
return String.fromCharCode(64 + maxAscii + 1);
}, [metricQueries]);
const handleFormulaChange = ({
formulaIndex,
expression,
toggleDisable,
toggleDelete,
}: IQueryBuilderFormulaHandleChange): void => {
const allFormulas = formulaQueries;
const current = allFormulas[formulaIndex];
if (expression) {
current.expression = expression;
}
if (toggleDisable) {
current.disabled = !current.disabled;
}
if (toggleDelete) {
delete allFormulas[formulaIndex];
} else {
allFormulas[formulaIndex] = current;
}
setFormulaQueries({
...allFormulas,
});
};
const handleMetricQueryChange = ({
queryIndex,
aggregateFunction,
metricName,
tagFilters,
groupBy,
legend,
toggleDisable,
toggleDelete,
}: IQueryBuilderQueryHandleChange): void => {
const allQueries = metricQueries;
const current = metricQueries[queryIndex];
if (aggregateFunction) {
current.aggregateOperator = aggregateFunction;
}
if (metricName) {
current.metricName = metricName;
}
if (tagFilters && current.tagFilters) {
current.tagFilters.items = tagFilters;
}
if (legend) {
current.legend = legend;
}
if (groupBy) {
current.groupBy = groupBy;
}
if (toggleDisable) {
current.disabled = !current.disabled;
}
if (toggleDelete) {
delete allQueries[queryIndex];
} else {
allQueries[queryIndex] = current;
}
setMetricQueries({
...allQueries,
});
};
const addMetricQuery = useCallback(() => {
if (Object.keys(metricQueries).length > 5) {
notification.error({
message: t('metric_query_max_limit'),
});
return;
}
const queryLabel = getNextQueryLabel();
const queries = metricQueries;
queries[queryLabel] = {
name: queryLabel,
queryName: queryLabel,
metricName: '',
formulaOnly: false,
aggregateOperator: EAggregateOperator.NOOP,
legend: '',
tagFilters: {
op: 'AND',
items: [],
},
groupBy: [],
disabled: false,
expression: queryLabel,
};
setMetricQueries({ ...queries });
}, [t, getNextQueryLabel, metricQueries, setMetricQueries]);
const addFormula = useCallback(() => {
// defaulting to F1 as only one formula is supported
// in alert definition
const queryLabel = 'F1';
const formulas = formulaQueries;
formulas[queryLabel] = {
queryName: queryLabel,
name: queryLabel,
formulaOnly: true,
expression: 'A',
disabled: false,
};
setFormulaQueries({ ...formulas });
}, [formulaQueries, setFormulaQueries]);
const renderPromqlUI = (): JSX.Element => {
return (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
};
const renderFormulaButton = (): JSX.Element => {
return (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
};
const renderQueryButton = (): JSX.Element => {
return (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
};
const renderMetricUI = (): JSX.Element => {
return (
<div>
{metricQueries &&
Object.keys(metricQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = metricQueries[key];
current.name = key;
return (
<MetricsBuilder
key={key}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
handleQueryChange={handleMetricQueryChange}
/>
);
})}
{queryCategory !== EQueryType.PROM && renderQueryButton()}
<div style={{ marginTop: '1rem' }}>
{formulaQueries &&
Object.keys(formulaQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = formulaQueries[key];
current.name = key;
return (
<MetricsBuilderFormula
key={key}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
/>
);
})}
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
</div>
</div>
);
};
return (
<>
<StepHeading> {t('alert_form_step1')}</StepHeading>
<FormContainer>
<div style={{ display: 'flex' }}>
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER.toString()}
activeKey={queryCategory.toString()}
onChange={handleQueryCategoryChange}
>
<TabPane tab={t('tab_qb')} key={EQueryType.QUERY_BUILDER.toString()} />
<TabPane tab={t('tab_promql')} key={EQueryType.PROM.toString()} />
</Tabs>
</div>
{queryCategory === EQueryType.PROM ? renderPromqlUI() : renderMetricUI()}
</FormContainer>
</>
);
}
interface QuerySectionProps {
queryCategory: EQueryType;
setQueryCategory: (n: EQueryType) => void;
metricQueries: IMetricQueries;
setMetricQueries: (b: IMetricQueries) => void;
formulaQueries: IFormulaQueries;
setFormulaQueries: (b: IFormulaQueries) => void;
promQueries: IPromQueries;
setPromQueries: (p: IPromQueries) => void;
}
export default QuerySection;

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('alerts');
const handleMatchOptChange = (value: string | unknown): void => {
const m = (value as string) || alertDef.condition?.matchType;
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
matchType: m,
},
});
};
const renderCompareOps = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultCompareOp}
value={alertDef.condition?.op}
style={{ minWidth: '120px' }}
onChange={(value: string | unknown): void => {
const newOp = (value as string) || '';
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
op: newOp,
},
});
}}
>
<Option value="1">{t('option_above')}</Option>
<Option value="2">{t('option_below')}</Option>
<Option value="3">{t('option_equal')}</Option>
<Option value="4">{t('option_notequal')}</Option>
</InlineSelect>
);
};
const renderThresholdMatchOpts = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
<Option value="2">{t('option_allthetimes')}</Option>
<Option value="3">{t('option_onaverage')}</Option>
<Option value="4">{t('option_intotal')}</Option>
</InlineSelect>
);
};
const renderPromMatchOpts = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
</InlineSelect>
);
};
const renderEvalWindows = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultEvalWindow}
style={{ minWidth: '120px' }}
value={alertDef.evalWindow}
onChange={(value: string | unknown): void => {
const ew = (value as string) || alertDef.evalWindow;
setAlertDef({
...alertDef,
evalWindow: ew,
});
}}
>
{' '}
<Option value="5m0s">{t('option_5min')}</Option>
<Option value="10m0s">{t('option_10min')}</Option>
<Option value="15m0s">{t('option_15min')}</Option>
<Option value="60m0s">{t('option_60min')}</Option>
<Option value="4h0m0s">{t('option_4hours')}</Option>
<Option value="24h0m0s">{t('option_24hours')}</Option>
</InlineSelect>
);
};
const renderThresholdRuleOpts = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
</Typography.Text>
</FormItem>
);
};
const renderPromRuleOptions = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderPromMatchOpts()}
</Typography.Text>
</FormItem>
);
};
return (
<>
<StepHeading>{t('alert_form_step2')}</StepHeading>
<FormContainer>
{queryCategory === EQueryType.PROM
? renderPromRuleOptions()
: renderThresholdRuleOpts()}
<div style={{ display: 'flex', alignItems: 'center' }}>
<ThresholdInput
controls={false}
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={(value: number | unknown): void => {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
target: (value as number) || undefined,
},
});
}}
/>
</div>
</FormContainer>
</>
);
}
interface RuleOptionsProps {
alertDef: AlertDef;
setAlertDef: (a: AlertDef) => void;
queryCategory: EQueryType;
}
export default RuleOptions;

View File

@ -0,0 +1,132 @@
import { Col, Row, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { EQueryType } from 'types/common/dashboard';
import {
StyledList,
StyledListItem,
StyledMainContainer,
StyledTopic,
} from './styles';
function UserGuide({ queryType }: UserGuideProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const renderStep1QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderGuideForQB = (): JSX.Element => {
return (
<>
{renderStep1QB()}
{renderStep2QB()}
{renderStep3QB()}
</>
);
};
const renderStep1PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderGuideForPQL = (): JSX.Element => {
return (
<>
{renderStep1PQL()}
{renderStep2PQL()}
{renderStep3PQL()}
</>
);
};
return (
<StyledMainContainer>
<Row>
<Col flex="auto">
<Typography.Paragraph> {t('user_guide_headline')} </Typography.Paragraph>
</Col>
<Col flex="none">
<TextToolTip
text={t('user_tooltip_more_help')}
url="https://signoz.io/docs/userguide/alerts-management/#create-alert-rules"
/>
</Col>
</Row>
{queryType === EQueryType.QUERY_BUILDER && renderGuideForQB()}
{queryType === EQueryType.PROM && renderGuideForPQL()}
</StyledMainContainer>
);
}
interface UserGuideProps {
queryType: EQueryType;
}
export default UserGuide;

View File

@ -0,0 +1,17 @@
import { Card, Typography } from 'antd';
import styled from 'styled-components';
export const StyledMainContainer = styled(Card)``;
export const StyledTopic = styled(Typography.Paragraph)`
font-weight: 600;
`;
export const StyledList = styled.ul`
padding-left: 18px;
`;
export const StyledListItem = styled.li`
font-style: italic;
padding-bottom: 0.5rem;
`;

View File

@ -0,0 +1,381 @@
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { FormInstance, Modal, notification, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import ROUTES from 'constants/routes';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import history from 'lib/history';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import {
IFormulaQueries,
IMetricQueries,
IPromQueries,
} from 'types/api/alerts/compositeQuery';
import {
AlertDef,
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
import { Query as StagedQuery } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import BasicInfo from './BasicInfo';
import ChartPreview from './ChartPreview';
import QuerySection from './QuerySection';
import RuleOptions from './RuleOptions';
import {
ActionButton,
ButtonContainer,
MainFormContainer,
PanelContainer,
StyledLeftContainer,
StyledRightContainer,
} from './styles';
import useDebounce from './useDebounce';
import UserGuide from './UserGuide';
import {
prepareBuilderQueries,
prepareStagedQuery,
toChartInterval,
toFormulaQueries,
toMetricQueries,
} from './utils';
function FormAlertRules({
formInstance,
initialValue,
ruleId,
}: FormAlertRuleProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
// use query client
const ruleCache = useQueryClient();
const [loading, setLoading] = useState(false);
// alertDef holds the form values to be posted
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
// initQuery contains initial query when component was mounted
const initQuery = initialValue?.condition?.compositeMetricQuery;
const [queryCategory, setQueryCategory] = useState<EQueryType>(
initQuery?.queryType,
);
// local state to handle metric queries
const [metricQueries, setMetricQueries] = useState<IMetricQueries>(
toMetricQueries(initQuery?.builderQueries),
);
// local state to handle formula queries
const [formulaQueries, setFormulaQueries] = useState<IFormulaQueries>(
toFormulaQueries(initQuery?.builderQueries),
);
// local state to handle promql queries
const [promQueries, setPromQueries] = useState<IPromQueries>({
...initQuery?.promQueries,
});
// staged query is used to display chart preview
const [stagedQuery, setStagedQuery] = useState<StagedQuery>();
const debouncedStagedQuery = useDebounce(stagedQuery, 500);
// this use effect initiates staged query and
// other queries based on server data.
// useful when fetching of initial values (from api)
// is delayed
useEffect(() => {
const initQuery = initialValue?.condition?.compositeMetricQuery;
const typ = initQuery?.queryType;
// extract metric query from builderQueries
const mq = toMetricQueries(initQuery?.builderQueries);
// extract formula query from builderQueries
const fq = toFormulaQueries(initQuery?.builderQueries);
// prepare staged query
const sq = prepareStagedQuery(typ, mq, fq, initQuery?.promQueries);
const pq = initQuery?.promQueries;
setQueryCategory(typ);
setMetricQueries(mq);
setFormulaQueries(fq);
setPromQueries(pq);
setStagedQuery(sq);
setAlertDef(initialValue);
}, [initialValue]);
// this useEffect updates staging query when
// any of its sub-parameters changes
useEffect(() => {
// prepare staged query
const sq: StagedQuery = prepareStagedQuery(
queryCategory,
metricQueries,
formulaQueries,
promQueries,
);
setStagedQuery(sq);
}, [queryCategory, metricQueries, formulaQueries, promQueries]);
const onCancelHandler = useCallback(() => {
history.replace(ROUTES.LIST_ALL_ALERT);
}, []);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults
const onQueryCategoryChange = (val: EQueryType): void => {
setQueryCategory(val);
if (val === EQueryType.PROM) {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
matchType: defaultMatchType,
},
evalWindow: defaultEvalWindow,
});
}
};
const isFormValid = useCallback((): boolean => {
let retval = true;
if (!alertDef.alert || alertDef.alert === '') {
notification.error({
message: 'Error',
description: t('alertname_required'),
});
return false;
}
if (
queryCategory === EQueryType.PROM &&
(!promQueries || Object.keys(promQueries).length === 0)
) {
notification.error({
message: 'Error',
description: t('promql_required'),
});
return false;
}
if (
(queryCategory === EQueryType.QUERY_BUILDER && !metricQueries) ||
Object.keys(metricQueries).length === 0
) {
notification.error({
message: 'Error',
description: t('condition_required'),
});
return false;
}
Object.keys(metricQueries).forEach((key) => {
if (metricQueries[key].metricName === '') {
retval = false;
notification.error({
message: 'Error',
description: t('metricname_missing', { where: metricQueries[key].name }),
});
}
});
Object.keys(formulaQueries).forEach((key) => {
if (formulaQueries[key].expression === '') {
retval = false;
notification.error({
message: 'Error',
description: t('expression_missing', formulaQueries[key].name),
});
}
});
return retval;
}, [t, alertDef, queryCategory, metricQueries, formulaQueries, promQueries]);
const saveRule = useCallback(async () => {
if (!isFormValid()) {
return;
}
const postableAlert: AlertDef = {
...alertDef,
source: window?.location.toString(),
ruleType:
queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule',
condition: {
...alertDef.condition,
compositeMetricQuery: {
builderQueries: prepareBuilderQueries(metricQueries, formulaQueries),
promQueries,
queryType: queryCategory,
},
},
};
setLoading(true);
try {
const apiReq =
ruleId && ruleId > 0
? { data: postableAlert, id: ruleId }
: { data: postableAlert };
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) {
notification.success({
message: 'Success',
description:
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
});
console.log('invalidting cache');
// invalidate rule in cache
ruleCache.invalidateQueries(['ruleId', ruleId]);
setTimeout(() => {
history.replace(ROUTES.LIST_ALL_ALERT);
}, 2000);
} else {
notification.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
console.log('save alert api failed:', e);
notification.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [
t,
isFormValid,
queryCategory,
ruleId,
alertDef,
metricQueries,
formulaQueries,
promQueries,
ruleCache,
]);
const onSaveHandler = useCallback(async () => {
const content = (
<Typography.Text>
{' '}
{t('confirm_save_content_part1')} <QueryTypeTag queryType={queryCategory} />{' '}
{t('confirm_save_content_part2')}
</Typography.Text>
);
Modal.confirm({
icon: <ExclamationCircleOutlined />,
title: t('confirm_save_title'),
centered: true,
content,
onOk() {
saveRule();
},
});
}, [t, saveRule, queryCategory]);
const renderBasicInfo = (): JSX.Element => (
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
);
const renderQBChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name=""
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
};
const renderPromChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
/>
);
};
return (
<>
{Element}
<PanelContainer>
<StyledLeftContainer flex="5 1 600px">
<MainFormContainer
initialValues={initialValue}
layout="vertical"
form={formInstance}
>
{queryCategory === EQueryType.QUERY_BUILDER && renderQBChartPreview()}
{queryCategory === EQueryType.PROM && renderPromChartPreview()}
<QuerySection
queryCategory={queryCategory}
setQueryCategory={onQueryCategoryChange}
metricQueries={metricQueries}
setMetricQueries={setMetricQueries}
formulaQueries={formulaQueries}
setFormulaQueries={setFormulaQueries}
promQueries={promQueries}
setPromQueries={setPromQueries}
/>
<RuleOptions
queryCategory={queryCategory}
alertDef={alertDef}
setAlertDef={setAlertDef}
/>
{renderBasicInfo()}
<ButtonContainer>
<ActionButton
loading={loading || false}
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
>
{ruleId > 0 ? t('button_savechanges') : t('button_createrule')}
</ActionButton>
<ActionButton
disabled={loading || false}
type="default"
onClick={onCancelHandler}
>
{ruleId === 0 && t('button_cancelchanges')}
{ruleId > 0 && t('button_discard')}
</ActionButton>
</ButtonContainer>
</MainFormContainer>
</StyledLeftContainer>
<StyledRightContainer flex="1 1 300px">
<UserGuide queryType={queryCategory} />
</StyledRightContainer>
</PanelContainer>
</>
);
}
interface FormAlertRuleProps {
formInstance: FormInstance;
initialValue: AlertDef;
ruleId: number;
}
export default FormAlertRules;

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('alerts');
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const [currentVal, setCurrentVal] = useState('');
const [staging, setStaging] = useState<string[]>([]);
const [queries, setQueries] = useState<ILabelRecord[]>(
initialValues ? flattenLabels(initialValues) : [],
);
const dispatchChanges = (updatedRecs: ILabelRecord[]): void => {
onSetLabels(prepareLabels(updatedRecs, initialValues));
setQueries(updatedRecs);
};
const [state, send] = useMachine(ResourceAttributesFilterMachine, {
actions: {
onSelectLabelKey: () => {},
onSelectLabelValue: () => {
if (currentVal !== '') {
setStaging((prevState) => [...prevState, currentVal]);
} else {
return;
}
setCurrentVal('');
},
onValidateQuery: (): void => {
if (currentVal === '') {
return;
}
const generatedQuery = createQuery([...staging, currentVal]);
if (generatedQuery) {
dispatchChanges([...queries, generatedQuery]);
setStaging([]);
setCurrentVal('');
send('RESET');
}
},
},
});
const handleFocus = (): void => {
if (state.value === 'Idle') {
send('NEXT');
}
};
const handleBlur = useCallback((): void => {
if (staging.length === 1 && staging[0] !== undefined) {
send('onBlur');
}
}, [send, staging]);
useEffect(() => {
handleBlur();
}, [handleBlur]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setCurrentVal(e.target?.value);
};
const handleClose = (key: string): void => {
dispatchChanges(queries.filter((queryData) => queryData.key !== key));
};
const handleClearAll = (): void => {
Modal.confirm({
title: 'Confirm',
icon: <ExclamationCircleOutlined />,
content: t('remove_label_confirm'),
onOk() {
send('RESET');
dispatchChanges([]);
setStaging([]);
message.success(t('remove_label_success'));
},
okText: t('button_yes'),
cancelText: t('button_no'),
});
};
const renderPlaceholder = useCallback((): string => {
if (state.value === 'LabelKey') return 'Enter a label key then press ENTER.';
if (state.value === 'LabelValue')
return `Enter a value for label key(${staging[0]}) then press ENTER.`;
return t('placeholder_label_key_pair');
}, [t, state, staging]);
return (
<SearchContainer isDarkMode={isDarkMode} disabled={false}>
<div style={{ display: 'inline-flex', flexWrap: 'wrap' }}>
{queries.length > 0 &&
map(
queries,
(query): JSX.Element => {
return (
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
);
},
)}
</div>
<div>
{map(staging, (item) => {
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
})}
</div>
<div style={{ display: 'flex', width: '100%' }}>
<Input
placeholder={renderPlaceholder()}
onChange={handleChange}
onKeyUp={(e): void => {
if (e.key === 'Enter' || e.code === 'Enter') {
send('NEXT');
}
}}
bordered={false}
value={currentVal as never}
style={{ flex: 1 }}
onFocus={handleFocus}
onBlur={handleBlur}
/>
{queries.length || staging.length || currentVal ? (
<Button
onClick={handleClearAll}
icon={<CloseCircleFilled />}
type="text"
/>
) : null}
</div>
</SearchContainer>
);
}
export default LabelSelect;

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,103 @@
import { Button, Card, Col, Form, Input, InputNumber, Row, Select } from 'antd';
import TextArea from 'antd/lib/input/TextArea';
import styled from 'styled-components';
export const PanelContainer = styled(Row)`
flex-wrap: nowrap;
`;
export const StyledRightContainer = styled(Col)`
&&& {
}
`;
export const StyledLeftContainer = styled(Col)`
&&& {
margin-right: 1rem;
}
`;
export const MainFormContainer = styled(Form)``;
export const ButtonContainer = styled.div`
&&& {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 1rem;
margin-bottom: 3rem;
}
`;
export const ActionButton = styled(Button)`
margin-right: 1rem;
`;
export const QueryButton = styled(Button)`
&&& {
display: flex;
align-items: center;
margin-right: 1rem;
}
`;
export const QueryContainer = styled(Card)`
&&& {
margin-top: 1rem;
min-height: 23.5%;
}
`;
export const Container = styled.div`
margin-top: 1rem;
display: flex;
flex-direction: column;
`;
export const StepHeading = styled.p`
margin-top: 1rem;
font-weight: bold;
`;
export const InlineSelect = styled(Select)`
display: inline-block;
width: 10% !important;
margin-left: 0.2em;
margin-right: 0.2em;
`;
export const SeveritySelect = styled(Select)`
width: 15% !important;
`;
export const InputSmall = styled(Input)`
width: 40% !important;
`;
export const FormContainer = styled.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

@ -39,6 +39,7 @@ function Trace(props: TraceProps): JSX.Element {
isExpandAll,
intervalUnit,
children,
isMissing,
} = props;
const { isDarkMode } = useThemeMode();
@ -125,7 +126,7 @@ function Trace(props: TraceProps): JSX.Element {
isDarkMode={isDarkMode}
/>
<CardContainer onClick={onClick}>
<CardContainer isMissing={isMissing} onClick={onClick}>
<StyledCol flex={`${panelWidth}px`} styledclass={[styles.overFlowHidden]}>
<StyledRow styledclass={[styles.flexNoWrap]}>
<Col>
@ -174,6 +175,7 @@ function Trace(props: TraceProps): JSX.Element {
activeSpanPath={activeSpanPath}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
isMissing={child.isMissing}
/>
))}
</>
@ -182,6 +184,10 @@ function Trace(props: TraceProps): JSX.Element {
);
}
Trace.defaultProps = {
isMissing: false,
};
interface ITraceGlobal {
globalSpread: ITraceMetaData['spread'];
globalStart: ITraceMetaData['globalStart'];
@ -196,6 +202,7 @@ interface TraceProps extends ITraceTree, ITraceGlobal {
activeSpanPath: string[];
isExpandAll: boolean;
intervalUnit: IIntervalUnit;
isMissing?: boolean;
}
export default Trace;

View File

@ -1,3 +1,4 @@
import { volcano } from '@ant-design/colors';
import styled, {
css,
DefaultTheme,
@ -15,7 +16,6 @@ export const Wrapper = styled.ul<Props>`
padding-top: 0.5rem;
position: relative;
z-index: 1;
ul {
border-left: ${({ isOnlyChild }): StyledCSS =>
isOnlyChild && 'none'} !important;
@ -36,10 +36,14 @@ export const Wrapper = styled.ul<Props>`
}
`;
export const CardContainer = styled.li`
export const CardContainer = styled.li<{ isMissing?: boolean }>`
display: flex;
width: 100%;
cursor: pointer;
border-radius: 0.25rem;
z-index: 2;
${({ isMissing }): string =>
isMissing ? `border: 1px dashed ${volcano[6]} !important;` : ''}
`;
interface Props {

View File

@ -3,7 +3,7 @@ import { IIntervalUnit } from 'container/TraceDetail/utils';
import React, { useEffect, useState } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { CardContainer, CardWrapper, CollapseButton, Wrapper } from './styles';
import { CardContainer, CardWrapper, CollapseButton } from './styles';
import Trace from './Trace';
import { getSpanPath } from './utils';
@ -36,35 +36,33 @@ function GanttChart(props: GanttChartProps): JSX.Element {
setIsExpandAll((prev) => !prev);
};
return (
<Wrapper>
<CardContainer>
<CollapseButton
onClick={handleCollapse}
title={isExpandAll ? 'Collapse All' : 'Expand All'}
>
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
</CollapseButton>
<CardWrapper>
<Trace
activeHoverId={activeHoverId}
activeSpanPath={activeSpanPath}
setActiveHoverId={setActiveHoverId}
key={data.id}
// eslint-disable-next-line react/jsx-props-no-spreading
{...{
...data,
globalSpread,
globalStart,
setActiveSelectedId,
activeSelectedId,
}}
level={0}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
/>
</CardWrapper>
</CardContainer>
</Wrapper>
<CardContainer>
<CollapseButton
onClick={handleCollapse}
title={isExpandAll ? 'Collapse All' : 'Expand All'}
>
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
</CollapseButton>
<CardWrapper>
<Trace
activeHoverId={activeHoverId}
activeSpanPath={activeSpanPath}
setActiveHoverId={setActiveHoverId}
key={data.id}
// eslint-disable-next-line react/jsx-props-no-spreading
{...{
...data,
globalSpread,
globalStart,
setActiveSelectedId,
activeSelectedId,
}}
level={0}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
/>
</CardWrapper>
</CardContainer>
);
}

View File

@ -38,6 +38,7 @@ export const CardWrapper = styled.div`
export const CardContainer = styled.li`
display: flex;
width: 100%;
position: relative;
`;
export const CollapseButton = styled.div`

View File

@ -1,4 +1,5 @@
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { set } from 'lodash-es';
import { ITraceForest, ITraceTree } from 'types/api/trace/getTraceItem';
interface GetTraceMetaData {
globalStart: number;
@ -65,25 +66,48 @@ export function getTopLeftFromBody(
export const getNodeById = (
searchingId: string,
treeData: ITraceTree,
): ITraceTree | undefined => {
let foundNode: ITraceTree | undefined;
const traverse = (treeNode: ITraceTree, level = 0): void => {
treesData: ITraceForest | undefined,
): ITraceForest => {
const newtreeData: ITraceForest = {} as ITraceForest;
const traverse = (
treeNode: ITraceTree,
setCallBack: (arg0: ITraceTree) => void,
level = 0,
): void => {
if (!treeNode) {
return;
}
if (searchingId === treeNode.id) {
foundNode = treeNode;
setCallBack(treeNode);
}
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1);
traverse(childNode, setCallBack, level + 1);
});
};
traverse(treeData, 1);
return foundNode;
const spanTreeSetCallback = (
path: (keyof ITraceForest)[],
value: ITraceTree,
): ITraceForest => set(newtreeData, path, [value]);
if (treesData?.spanTree)
treesData.spanTree.forEach((tree) => {
traverse(tree, (value) => spanTreeSetCallback(['spanTree'], value), 1);
});
if (treesData?.missingSpanTree)
treesData.missingSpanTree.forEach((tree) => {
traverse(
tree,
(value) => spanTreeSetCallback(['missingSpanTree'], value),
1,
);
});
return newtreeData;
};
const getSpanWithoutChildren = (

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,15 +29,15 @@ function PromQLQueryContainer({
toggleDelete,
}: IPromQLQueryHandleChange): void => {
const allQueries = queryData[WIDGET_PROMQL_QUERY_KEY_NAME];
const currentIndexQuery = allQueries[queryIndex];
if (query) currentIndexQuery.query = query;
if (legend) currentIndexQuery.legend = legend;
const currentIndexQuery = allQueries[queryIndex as number];
if (query !== undefined) currentIndexQuery.query = query;
if (legend !== undefined) currentIndexQuery.legend = legend;
if (toggleDisable) {
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,12 +50,12 @@ 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;
}
if (metricName) {
if (metricName !== undefined) {
currentIndexQuery.metricName = metricName;
}
@ -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

@ -0,0 +1,41 @@
import { volcano } from '@ant-design/colors';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Popover } from 'antd';
import React from 'react';
function PopOverContent(): JSX.Element {
return (
<div>
More details on missing spans{' '}
<a
href="https://signoz.io/docs/userguide/traces/#missing-spans"
rel="noopener noreferrer"
target="_blank"
>
here
</a>
</div>
);
}
function MissingSpansMessage(): JSX.Element {
return (
<Popover content={PopOverContent} trigger="hover" placement="bottom">
<div
style={{
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
margin: '1rem 0',
fontSize: '0.8rem',
}}
>
<InfoCircleOutlined style={{ color: volcano[6], marginRight: '0.3rem' }} />{' '}
This trace has missing spans
</div>
</Popover>
);
}
export default MissingSpansMessage;

View File

@ -0,0 +1,53 @@
import { StyledButton } from 'components/Styled';
import React from 'react';
import { styles } from './styles';
function EllipsedButton({
onToggleHandler,
setText,
value,
event,
buttonText,
}: Props): JSX.Element {
const isFullValueButton = buttonText === 'View full value';
const style = [styles.removePadding];
if (!isFullValueButton) {
style.push(styles.removeMargin);
} else {
style.push(styles.selectedSpanDetailsContainer);
style.push(styles.buttonContainer);
}
return (
<StyledButton
styledclass={style}
onClick={(): void => {
onToggleHandler(true);
setText({
subText: value,
text: event,
});
}}
type="link"
>
{buttonText}
</StyledButton>
);
}
interface Props {
onToggleHandler: (isOpen: boolean) => void;
setText: (text: { subText: string; text: string }) => void;
value: string;
event: string;
buttonText?: string;
}
EllipsedButton.defaultProps = {
buttonText: 'View full log event message',
};
export default EllipsedButton;

View File

@ -1,29 +1,22 @@
import { Collapse, Modal } from 'antd';
import Editor from 'components/Editor';
import { StyledButton } from 'components/Styled';
import { Collapse } from 'antd';
import useThemeMode from 'hooks/useThemeMode';
import keys from 'lodash-es/keys';
import map from 'lodash-es/map';
import React, { useState } from 'react';
import React from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { CustomSubText, CustomSubTitle, styles } from './styles';
import EllipsedButton from './EllipsedButton';
import { CustomSubText, CustomSubTitle } from './styles';
const { Panel } = Collapse;
function ErrorTag({ event }: ErrorTagProps): JSX.Element {
const [isOpen, setIsOpen] = useState(false);
function ErrorTag({
event,
onToggleHandler,
setText,
}: ErrorTagProps): JSX.Element {
const { isDarkMode } = useThemeMode();
const [text, setText] = useState({
text: '',
subText: '',
});
const onToggleHandler = (state: boolean): void => {
setIsOpen(state);
};
return (
<>
{map(event, ({ attributeMap, name }) => {
@ -45,23 +38,23 @@ function ErrorTag({ event }: ErrorTagProps): JSX.Element {
return (
<>
<CustomSubTitle>{event}</CustomSubTitle>
<CustomSubText ellipsis={isEllipsed} isDarkMode={isDarkMode}>
<CustomSubText
ellipsis={{
rows: isEllipsed ? 1 : 0,
}}
isDarkMode={isDarkMode}
>
{value}
<br />
{isEllipsed && (
<StyledButton
styledclass={[styles.removeMargin, styles.removePadding]}
onClick={(): void => {
onToggleHandler(true);
setText({
subText: value,
text: event,
});
<EllipsedButton
{...{
event,
onToggleHandler,
setText,
value,
}}
type="link"
>
View full log event message
</StyledButton>
/>
)}
</CustomSubText>
</>
@ -71,31 +64,14 @@ function ErrorTag({ event }: ErrorTagProps): JSX.Element {
</Collapse>
);
})}
<Modal
onCancel={(): void => onToggleHandler(false)}
title="Log Message"
visible={isOpen}
destroyOnClose
footer={[]}
width="70vw"
>
<CustomSubTitle>{text.text}</CustomSubTitle>
{text.text === 'exception.stacktrace' ? (
<Editor onChange={(): void => {}} readOnly value={text.subText} />
) : (
<CustomSubText ellipsis={false} isDarkMode={isDarkMode}>
{text.subText}
</CustomSubText>
)}
</Modal>
</>
);
}
interface ErrorTagProps {
event: ITraceTree['event'];
onToggleHandler: (isOpen: boolean) => void;
setText: (text: { subText: string; text: string }) => void;
}
export default ErrorTag;

View File

@ -1,9 +1,11 @@
import { Tabs, Tooltip, Typography } from 'antd';
import { Modal, Tabs, Tooltip, Typography } from 'antd';
import Editor from 'components/Editor';
import { StyledSpace } from 'components/Styled';
import useThemeMode from 'hooks/useThemeMode';
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import EllipsedButton from './EllipsedButton';
import ErrorTag from './ErrorTag';
import {
CardContainer,
@ -12,12 +14,14 @@ import {
CustomText,
CustomTitle,
styles,
SubTextContainer,
} from './styles';
const { TabPane } = Tabs;
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
const { tree } = props;
const { isDarkMode } = useThemeMode();
const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]);
@ -25,6 +29,17 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
tree?.serviceName,
]);
const [isOpen, setIsOpen] = useState(false);
const [text, setText] = useState({
text: '',
subText: '',
});
const onToggleHandler = (state: boolean): void => {
setIsOpen(state);
};
if (!tree) {
return <div />;
}
@ -51,18 +66,60 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
</Tooltip>
</StyledSpace>
<Modal
onCancel={(): void => onToggleHandler(false)}
title={text.text}
visible={isOpen}
destroyOnClose
footer={[]}
width="70vw"
centered
>
{text.text === 'exception.stacktrace' ? (
<Editor onChange={(): void => {}} readOnly value={text.subText} />
) : (
<CustomSubText ellipsis={false} isDarkMode={isDarkMode}>
{text.subText}
</CustomSubText>
)}
</Modal>
<Tabs defaultActiveKey="1">
<TabPane tab="Tags" key="1">
{tags.length !== 0 ? (
tags.map((tags) => {
const value = tags.key === 'error' ? 'true' : tags.value;
const isEllipsed = value.length > 24;
return (
<React.Fragment key={JSON.stringify(tags)}>
{tags.value && (
<>
<CustomSubTitle>{tags.key}</CustomSubTitle>
<CustomSubText isDarkMode={isDarkMode}>
{tags.key === 'error' ? 'true' : tags.value}
</CustomSubText>
<SubTextContainer isDarkMode={isDarkMode}>
<Tooltip overlay={(): string => value}>
<CustomSubText
ellipsis={{
rows: isEllipsed ? 1 : 0,
}}
isDarkMode={isDarkMode}
>
{value}
</CustomSubText>
{isEllipsed && (
<EllipsedButton
{...{
event: tags.key,
onToggleHandler,
setText,
value,
buttonText: 'View full value',
}}
/>
)}
</Tooltip>
</SubTextContainer>
</>
)}
</React.Fragment>
@ -74,7 +131,11 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
</TabPane>
<TabPane tab="Events" key="2">
{tree.event && Object.keys(tree.event).length !== 0 ? (
<ErrorTag event={tree.event} />
<ErrorTag
onToggleHandler={onToggleHandler}
setText={setText}
event={tree.event}
/>
) : (
<Typography>No events data in selected span</Typography>
)}

View File

@ -18,7 +18,8 @@ export const CustomText = styled(Paragraph)`
export const CustomSubTitle = styled(Title)`
&&& {
font-size: 14px;
margin-bottom: 8px;
margin-bottom: 0.1rem;
margin-top: 0.5rem;
}
`;
@ -26,13 +27,19 @@ interface CustomSubTextProps {
isDarkMode: boolean;
}
export const SubTextContainer = styled.div<CustomSubTextProps>`
&&& {
background: ${({ isDarkMode }): string => (isDarkMode ? '#444' : '#ddd')};
}
`;
export const CustomSubText = styled(Paragraph)<CustomSubTextProps>`
&&& {
background: ${({ isDarkMode }): string => (isDarkMode ? '#444' : '#ddd')};
font-size: 12px;
padding: 6px 8px;
padding: 4px 8px;
word-break: break-all;
margin-bottom: 16px;
margin-bottom: 0rem;
}
`;
@ -81,10 +88,15 @@ const overflow = css`
}
`;
const buttonContainer = css`
height: 1.5rem;
`;
export const styles = {
removeMargin,
removePadding,
selectedSpanDetailsContainer,
spanEventsTabsContainer,
overflow,
buttonContainer,
};

View File

@ -17,15 +17,23 @@ import dayjs from 'dayjs';
import useUrlQuery from 'hooks/useUrlQuery';
import { spanServiceNameToColorMapping } from 'lib/getRandomColor';
import history from 'lib/history';
import { map } from 'lodash-es';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import React, { useEffect, useMemo, useState } from 'react';
import { ITraceTree, PayloadProps } from 'types/api/trace/getTraceItem';
import { ITraceForest, PayloadProps } from 'types/api/trace/getTraceItem';
import { getSpanTreeMetadata } from 'utils/getSpanTreeMetadata';
import { spanToTreeUtil } from 'utils/spanToTree';
import MissingSpansMessage from './Missingtrace';
import SelectedSpanDetails from './SelectedSpanDetails';
import * as styles from './styles';
import { getSortedData, IIntervalUnit, INTERVAL_UNITS } from './utils';
import { FlameGraphMissingSpansContainer, GanttChartWrapper } from './styles';
import {
getSortedData,
getTreeLevelsCount,
IIntervalUnit,
INTERVAL_UNITS,
} from './utils';
function TraceDetail({ response }: TraceDetailProps): JSX.Element {
const spanServiceColors = useMemo(
@ -43,17 +51,23 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
const [activeHoverId, setActiveHoverId] = useState<string>('');
const [activeSelectedId, setActiveSelectedId] = useState<string>(spanId || '');
const [treeData, setTreeData] = useState<ITraceTree>(
const [treesData, setTreesData] = useState<ITraceForest>(
spanToTreeUtil(response[0].events),
);
const { treeData: tree, ...traceMetaData } = useMemo(() => {
const tree = getSortedData(treeData);
const { treesData: tree, ...traceMetaData } = useMemo(() => {
const sortedTreesData: ITraceForest = {
spanTree: map(treesData.spanTree, (tree) => getSortedData(tree)),
missingSpanTree: map(
treesData.missingSpanTree,
(tree) => getSortedData(tree) || [],
),
};
// Note: Handle undefined
/*eslint-disable */
return getSpanTreeMetadata(tree as ITraceTree, spanServiceColors);
return getSpanTreeMetadata(sortedTreesData, spanServiceColors);
/* eslint-enable */
}, [treeData, spanServiceColors]);
}, [treesData, spanServiceColors]);
const [globalTraceMetadata] = useState<ITraceMetaData>({
...traceMetaData,
@ -69,24 +83,34 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
}, [activeSelectedId]);
const getSelectedNode = useMemo(() => {
return getNodeById(activeSelectedId, treeData);
}, [activeSelectedId, treeData]);
return getNodeById(activeSelectedId, treesData);
}, [activeSelectedId, treesData]);
// const onSearchHandler = (value: string) => {
// setSearchSpanString(value);
// setTreeData(spanToTreeUtil(response[0].events));
// };
const onFocusSelectedSpanHandler = (): void => {
const treeNode = getNodeById(activeSelectedId, tree);
if (treeNode) {
setTreeData(treeNode);
setTreesData(treeNode);
}
};
const onResetHandler = (): void => {
setTreeData(spanToTreeUtil(response[0].events));
setTreesData(spanToTreeUtil(response[0].events));
};
const hasMissingSpans = useMemo(
(): boolean =>
tree.missingSpanTree &&
Array.isArray(tree.missingSpanTree) &&
tree.missingSpanTree.length > 0,
[tree],
);
return (
<StyledRow styledclass={[StyledStyles.Flex({ flex: 1 })]}>
<StyledCol flex="auto" styledclass={styles.leftContainer}>
@ -101,16 +125,45 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
<StyledTypography.Text styledclass={[styles.removeMargin]}>
{traceMetaData.totalSpans} Span
</StyledTypography.Text>
{hasMissingSpans && <MissingSpansMessage />}
</StyledCol>
<Col flex="auto">
<TraceFlameGraph
treeData={tree}
traceMetaData={traceMetaData}
hoveredSpanId={activeHoverId}
selectedSpanId={activeSelectedId}
onSpanHover={setActiveHoverId}
onSpanSelect={setActiveSelectedId}
/>
{map(tree.spanTree, (tree) => {
return (
<TraceFlameGraph
key={tree as never}
treeData={tree}
traceMetaData={traceMetaData}
hoveredSpanId={activeHoverId}
selectedSpanId={activeSelectedId}
onSpanHover={setActiveHoverId}
onSpanSelect={setActiveSelectedId}
missingSpanTree={false}
/>
);
})}
{hasMissingSpans && (
<FlameGraphMissingSpansContainer>
{map(tree.missingSpanTree, (tree) => {
return (
<TraceFlameGraph
key={tree as never}
treeData={tree}
traceMetaData={{
...traceMetaData,
levels: getTreeLevelsCount(tree),
}}
hoveredSpanId={activeHoverId}
selectedSpanId={activeSelectedId}
onSpanHover={setActiveHoverId}
onSpanSelect={setActiveSelectedId}
missingSpanTree
/>
);
})}
</FlameGraphMissingSpansContainer>
)}
</Col>
</StyledRow>
<StyledRow styledclass={[styles.traceDateAndTimelineContainer]}>
@ -122,7 +175,9 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
justifyContent: 'center',
}}
>
{tree && dayjs(tree.startTime).format('hh:mm:ss a MM/DD')}
{tree &&
traceMetaData.globalStart &&
dayjs(traceMetaData.globalStart).format('hh:mm:ss a MM/DD')}
</StyledCol>
<StyledCol flex="auto" styledclass={[styles.timelineContainer]}>
<Timeline
@ -141,14 +196,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
}),
]}
>
<Col flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`}>
{/* <Search
placeholder="Type to filter.."
allowClear
onSearch={onSearchHandler}
style={{ width: 200 }}
/> */}
</Col>
<Col flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`} />
<Col flex="auto">
<StyledSpace styledclass={[styles.floatRight]}>
<Button onClick={onFocusSelectedSpanHandler} icon={<FilterOutlined />}>
@ -161,23 +209,50 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
</Col>
</StyledRow>
<StyledDiv styledclass={[styles.ganttChartContainer]}>
<GanttChart
traceMetaData={traceMetaData}
data={tree}
activeSelectedId={activeSelectedId}
activeHoverId={activeHoverId}
setActiveHoverId={setActiveHoverId}
setActiveSelectedId={setActiveSelectedId}
spanId={spanId || ''}
intervalUnit={intervalUnit}
/>
<GanttChartWrapper>
{map([...tree.spanTree, ...tree.missingSpanTree], (tree) => (
<GanttChart
key={tree as never}
traceMetaData={traceMetaData}
data={tree}
activeSelectedId={activeSelectedId}
activeHoverId={activeHoverId}
setActiveHoverId={setActiveHoverId}
setActiveSelectedId={setActiveSelectedId}
spanId={spanId || ''}
intervalUnit={intervalUnit}
/>
))}
{/* {map(tree.missingSpanTree, (tree) => (
<GanttChart
key={tree as never}
traceMetaData={traceMetaData}
data={tree}
activeSelectedId={activeSelectedId}
activeHoverId={activeHoverId}
setActiveHoverId={setActiveHoverId}
setActiveSelectedId={setActiveSelectedId}
spanId={spanId || ''}
intervalUnit={intervalUnit}
/>
))} */}
</GanttChartWrapper>
</StyledDiv>
</StyledCol>
<Col>
<StyledDivider styledclass={[styles.verticalSeparator]} type="vertical" />
</Col>
<StyledCol md={5} sm={5} styledclass={[styles.selectedSpanDetailContainer]}>
<SelectedSpanDetails tree={getSelectedNode} />
<SelectedSpanDetails
tree={[
...(getSelectedNode.spanTree ? getSelectedNode.spanTree : []),
...(getSelectedNode.missingSpanTree
? getSelectedNode.missingSpanTree
: []),
]
.filter(Boolean)
.find((tree) => tree)}
/>
</StyledCol>
</StyledRow>
);

View File

@ -1,4 +1,5 @@
import { css } from 'styled-components';
import { volcano } from '@ant-design/colors';
import styled, { css } from 'styled-components';
/**
* Styles for the left container. Containers flamegraph, timeline and gantt chart
@ -76,3 +77,38 @@ export const floatRight = css`
export const removeMargin = css`
margin: 0;
`;
export const GanttChartWrapper = styled.ul`
padding-left: 0;
position: absolute;
width: 100%;
height: 100%;
ul {
list-style: none;
border-left: 1px solid #434343;
padding-left: 1rem;
width: 100%;
}
ul li {
position: relative;
&:before {
position: absolute;
left: -1rem;
top: 10px;
content: '';
height: 1px;
width: 1rem;
background-color: #434343;
}
}
`;
export const FlameGraphMissingSpansContainer = styled.div`
border: 1px dashed ${volcano[6]};
padding: 0.5rem 0;
margin-top: 1rem;
border-radius: 0.25rem;
`;

View File

@ -62,7 +62,7 @@ export const convertTimeToRelevantUnit = (
return relevantTime;
};
export const getSortedData = (treeData: ITraceTree): undefined | ITraceTree => {
export const getSortedData = (treeData: ITraceTree): ITraceTree => {
const traverse = (treeNode: ITraceTree, level = 0): void => {
if (!treeNode) {
return;
@ -80,3 +80,21 @@ export const getSortedData = (treeData: ITraceTree): undefined | ITraceTree => {
return treeData;
};
export const getTreeLevelsCount = (tree: ITraceTree): number => {
let levels = 0;
const traverse = (treeNode: ITraceTree, level: number): void => {
if (!treeNode) {
return;
}
levels = Math.max(level, levels);
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1);
});
};
traverse(tree, levels);
return levels;
};

View File

@ -28,6 +28,7 @@ test('loads and displays greeting', () => {
spread: 0,
totalSpans: 0,
},
missingSpanTree: false,
treeData: {
children: [],
id: '',

View File

@ -93,8 +93,9 @@ function TraceFlameGraph(props: {
onSpanSelect: SpanItemProps['onSpanSelect'];
hoveredSpanId: string;
selectedSpanId: string;
missingSpanTree: boolean;
}): JSX.Element {
const { treeData, traceMetaData, onSpanHover } = props;
const { treeData, traceMetaData, onSpanHover, missingSpanTree } = props;
if (!treeData || treeData.id === 'empty' || !traceMetaData) {
return <div />;
@ -140,6 +141,7 @@ function TraceFlameGraph(props: {
hoveredSpanId={hoveredSpanId}
selectedSpanId={selectedSpanId}
/>
{spanData.children.map((childData) => (
<RenderSpanRecursive
level={level + 1}
@ -164,7 +166,7 @@ function TraceFlameGraph(props: {
onSpanSelect={onSpanSelect}
hoveredSpanId={hoveredSpanId}
selectedSpanId={selectedSpanId}
level={0}
level={missingSpanTree ? -1 : 0}
parentLeftOffset={0}
/>
</TraceFlameGraphContainer>

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

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