Merge branch 'develop' into store-fix-1

This commit is contained in:
Palash 2022-06-24 00:27:38 +05:30 committed by GitHub
commit 2ed24df250
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 5774 additions and 1426 deletions

3
.github/CODEOWNERS vendored
View File

@ -2,5 +2,6 @@
# Owners are automatically requested for review for PRs that changes code # Owners are automatically requested for review for PRs that changes code
# that they own. # that they own.
* @ankitnayan * @ankitnayan
/frontend/ @palash-signoz @pranshuchittora /frontend/ @palashgdev @pranshuchittora
/deploy/ @prashant-shahi /deploy/ @prashant-shahi
/pkg/query-service/ @srikanthccv @makeavish @nityanandagohain

View File

@ -17,6 +17,8 @@ jobs:
run: cd frontend && yarn install run: cd frontend && yarn install
- name: Run ESLint - name: Run ESLint
run: cd frontend && npm run lint run: cd frontend && npm run lint
- name: Run Jest
run: cd frontend && npm run jest
- name: TSC - name: TSC
run: yarn tsc run: yarn tsc
working-directory: ./frontend working-directory: ./frontend

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
<?xml version="1.0"?>
<clickhouse>
<storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration>
</clickhouse>

View File

@ -0,0 +1,123 @@
<?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

@ -2,12 +2,14 @@ version: "3.9"
services: services:
clickhouse: clickhouse:
image: yandex/clickhouse-server:21.12.3.32 image: clickhouse/clickhouse-server:22.4.5-alpine
# ports: # ports:
# - "9000:9000" # - "9000:9000"
# - "8123:8123" # - "8123:8123"
volumes: volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/ - ./data/clickhouse/:/var/lib/clickhouse/
deploy: deploy:
restart_policy: restart_policy:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
<?xml version="1.0"?>
<clickhouse>
<storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration>
</clickhouse>

View File

@ -0,0 +1,123 @@
<?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>

File diff suppressed because it is too large Load Diff

View File

@ -1,133 +0,0 @@
version: "2.4"
services:
clickhouse:
image: altinity/clickhouse-server:21.12.3.32.altinitydev.arm
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
interval: 30s
timeout: 5s
retries: 3
alertmanager:
image: signoz/alertmanager:0.23.0-0.1
volumes:
- ./data/alertmanager:/data
depends_on:
query-service:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8085
- --storage.path=/data
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:0.8.2
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
# - "8080:8080" # query-service port
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
- ./data/signoz/:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-arm
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
clickhouse:
condition: service_healthy
frontend:
image: signoz/frontend:0.8.2
container_name: frontend
restart: on-failure
depends_on:
- alertmanager
- query-service
ports:
- "3301:3301"
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.45.1-0.3
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "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
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
# - "55680:55680" # OTLP gRPC legacy receiver
# - "55681:55681" # OTLP HTTP legacy receiver
mem_limit: 2000m
restart: on-failure
depends_on:
clickhouse:
condition: service_healthy
otel-collector-metrics:
image: signoz/otelcontribcol:0.45.1-0.3
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
restart: on-failure
depends_on:
clickhouse:
condition: service_healthy
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
container_name: load-hotrod
hostname: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../common/locust-scripts:/locust

View File

@ -2,12 +2,14 @@ version: "2.4"
services: services:
clickhouse: clickhouse:
image: yandex/clickhouse-server:21.12.3.32 image: clickhouse/clickhouse-server:22.4.5-alpine
# ports: # ports:
# - "9000:9000" # - "9000:9000"
# - "8123:8123" # - "8123:8123"
volumes: volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/ - ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure restart: on-failure
logging: logging:

View File

@ -0,0 +1,123 @@
<?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

@ -11,6 +11,11 @@ server {
gzip_buffers 16 8k; gzip_buffers 16 8k;
gzip_http_version 1.1; gzip_http_version 1.1;
# to handle uri issue 414 from nginx
client_max_body_size 24M;
large_client_header_buffers 8 16k;
location / { location / {
if ( $uri = '/index.html' ) { if ( $uri = '/index.html' ) {
add_header Cache-Control no-store always; add_header Cache-Control no-store always;

View File

@ -2,3 +2,4 @@
* Adds custom matchers from the react testing library to all tests * Adds custom matchers from the react testing library to all tests
*/ */
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import 'jest-styled-components';

View File

@ -159,6 +159,7 @@
"husky": "^7.0.4", "husky": "^7.0.4",
"is-ci": "^3.0.1", "is-ci": "^3.0.1",
"jest-playwright-preset": "^1.7.0", "jest-playwright-preset": "^1.7.0",
"jest-styled-components": "^7.0.8",
"less-plugin-npm-import": "^2.1.0", "less-plugin-npm-import": "^2.1.0",
"lint-staged": "^12.3.7", "lint-staged": "^12.3.7",
"portfinder-sync": "^0.0.2", "portfinder-sync": "^0.0.2",

View File

@ -2,8 +2,102 @@
exports[`Not Found page test should render Not Found page without errors 1`] = ` exports[`Not Found page test should render Not Found page without errors 1`] = `
<DocumentFragment> <DocumentFragment>
<div .c3 {
class="sc-gsDKAQ cLXpIa" border: 2px solid #2f80ed;
box-sizing: border-box;
border-radius: 10px;
width: 400px;
background: inherit;
font-style: normal;
font-weight: normal;
font-size: 24px;
line-height: 20px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
padding-top: 14px;
padding-bottom: 14px;
color: #2f80ed;
}
.c0 {
min-height: 80vh;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c2 {
font-style: normal;
font-weight: 300;
font-size: 18px;
line-height: 20px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
color: #828282;
text-align: center;
margin: 0;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c1 {
min-height: 50px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
margin-bottom: 30px;
margin-top: 20px;
}
<div
class="c0"
> >
<svg <svg
fill="none" fill="none"
@ -272,21 +366,21 @@ exports[`Not Found page test should render Not Found page without errors 1`] = `
</defs> </defs>
</svg> </svg>
<div <div
class="sc-hKwDye foaleg" class="c1"
> >
<p <p
class="sc-dkPtRN fcyVIq" class="c2"
> >
Ah, seems like we reached a dead end! Ah, seems like we reached a dead end!
</p> </p>
<p <p
class="sc-dkPtRN fcyVIq" class="c2"
> >
Page Not Found Page Not Found
</p> </p>
</div> </div>
<a <a
class="sc-bdvvtL dbTZkj" class="c3"
href="/application" href="/application"
tabindex="0" tabindex="0"
> >

View File

@ -12,7 +12,7 @@ const ROUTES = {
ALL_DASHBOARD: '/dashboard', ALL_DASHBOARD: '/dashboard',
DASHBOARD: '/dashboard/:dashboardId', DASHBOARD: '/dashboard/:dashboardId',
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId', DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
EDIT_ALERTS: '/alerts/edit/:ruleId', EDIT_ALERTS: '/alerts/edit',
LIST_ALL_ALERT: '/alerts', LIST_ALL_ALERT: '/alerts',
ALERTS_NEW: '/alerts/new', ALERTS_NEW: '/alerts/new',
ALL_CHANNELS: '/settings/channels', ALL_CHANNELS: '/settings/channels',

View File

@ -57,7 +57,10 @@ function FullView({
time: timePreferenceType, time: timePreferenceType,
): { min: string | number; max: string | number } => { ): { min: string | number; max: string | number } => {
if (time === 'GLOBAL_TIME') { if (time === 'GLOBAL_TIME') {
const minMax = GetMinMax(globalSelectedTime); const minMax = GetMinMax(globalSelectedTime, [
minTime / 1000000,
maxTime / 1000000,
]);
return { return {
min: convertToNanoSecondsToSecond(minMax.minTime / 1000), min: convertToNanoSecondsToSecond(minMax.minTime / 1000),
max: convertToNanoSecondsToSecond(minMax.maxTime / 1000), max: convertToNanoSecondsToSecond(minMax.maxTime / 1000),

View File

@ -218,7 +218,7 @@ function GridGraph(props: Props): JSX.Element {
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => { const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
setLayoutFunction(layout); setLayoutFunction(layout);
await onLayoutSaveHandler(layout); // await onLayoutSaveHandler(layout);
}; };
const onAddPanelHandler = useCallback(() => { const onAddPanelHandler = useCallback(() => {

View File

@ -11,7 +11,6 @@ import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query'; import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { Alerts } from 'types/api/alerts/getAll'; import { Alerts } from 'types/api/alerts/getAll';
@ -51,11 +50,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const [notifications, Element] = notification.useNotification(); const [notifications, Element] = notification.useNotification();
const onEditHandler = (id: string): void => { const onEditHandler = (id: string): void => {
history.push( history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`);
generatePath(ROUTES.EDIT_ALERTS, {
ruleId: id,
}),
);
}; };
const columns: ColumnsType<Alerts> = [ const columns: ColumnsType<Alerts> = [

View File

@ -0,0 +1,58 @@
import { Dashboard } from 'types/api/dashboard/getAll';
import { v4 as uuid } from 'uuid';
import { TOperator } from '../types';
import { executeSearchQueries } from '../utils';
describe('executeSearchQueries', () => {
const firstDashboard: Dashboard = {
id: 11111,
uuid: uuid(),
created_at: '',
updated_at: '',
data: {
title: 'first dashboard',
},
};
const secondDashboard: Dashboard = {
id: 22222,
uuid: uuid(),
created_at: '',
updated_at: '',
data: {
title: 'second dashboard',
},
};
const thirdDashboard: Dashboard = {
id: 333333,
uuid: uuid(),
created_at: '',
updated_at: '',
data: {
title: 'third dashboard (with special characters +?\\)',
},
};
const dashboards = [firstDashboard, secondDashboard, thirdDashboard];
it('should filter dashboards based on title', () => {
const query = {
category: 'title',
id: 'someid',
operator: '=' as TOperator,
value: 'first dashboard',
};
expect(executeSearchQueries([query], dashboards)).toEqual([firstDashboard]);
});
it('should filter dashboards with special characters', () => {
const query = {
category: 'title',
id: 'someid',
operator: '=' as TOperator,
value: 'third dashboard (with special characters +?\\)',
};
expect(executeSearchQueries([query], dashboards)).toEqual([thirdDashboard]);
});
});

View File

@ -42,6 +42,8 @@ export const executeSearchQueries = (
if (!searchData.length || !queries.length) { if (!searchData.length || !queries.length) {
return searchData; return searchData;
} }
const escapeRegExp = (regExp: string): string =>
regExp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
queries.forEach((query: IQueryStructure) => { queries.forEach((query: IQueryStructure) => {
const { operator } = query; const { operator } = query;
@ -61,7 +63,7 @@ export const executeSearchQueries = (
for (const searchSpaceItem of searchSpace) { for (const searchSpaceItem of searchSpace) {
if (searchSpaceItem) if (searchSpaceItem)
for (const queryValue of value) { for (const queryValue of value) {
if (searchSpaceItem.match(queryValue)) { if (searchSpaceItem.match(escapeRegExp(queryValue))) {
return resolveOperator(true, operator); return resolveOperator(true, operator);
} }
} }

View File

@ -15,7 +15,7 @@ const menus: SidebarMenu[] = [
{ {
Icon: BarChartOutlined, Icon: BarChartOutlined,
to: ROUTES.APPLICATION, to: ROUTES.APPLICATION,
name: 'Metrics', name: 'Services',
}, },
{ {
Icon: AlignLeftOutlined, Icon: AlignLeftOutlined,

View File

@ -16,6 +16,7 @@ const breadcrumbNameMap = {
[ROUTES.ORG_SETTINGS]: 'Organization Settings', [ROUTES.ORG_SETTINGS]: 'Organization Settings',
[ROUTES.MY_SETTINGS]: 'My Settings', [ROUTES.MY_SETTINGS]: 'My Settings',
[ROUTES.ERROR_DETAIL]: 'Errors', [ROUTES.ERROR_DETAIL]: 'Errors',
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
}; };
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element { function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@ -1,12 +1,19 @@
import React from 'react'; import { Button, Input } from 'antd';
import { useSelector } from 'react-redux'; import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { INITIAL_FILTER_VALUE } from 'store/reducers/trace';
import AppActions from 'types/actions';
import { UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE } from 'types/actions/trace';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import CheckBoxComponent from '../Common/Checkbox'; import CheckBoxComponent from '../Common/Checkbox';
const { Search } = Input;
function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element { function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
const { filter } = useSelector<AppState, TraceReducer>( const { filter, filterDisplayValue } = useSelector<AppState, TraceReducer>(
(state) => state.traces, (state) => state.traces,
); );
@ -15,9 +22,40 @@ function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
const status = filter.get(name) || {}; const status = filter.get(name) || {};
const statusObj = Object.keys(status); const statusObj = Object.keys(status);
const numberOfFilters = filterDisplayValue.get(name) || 0;
const dispatch = useDispatch<Dispatch<AppActions>>();
const [searchFilter, setSearchFilter] = useState<string>('');
const onClickMoreHandler = (): void => {
const newFilterDisplayValue = new Map(filterDisplayValue);
const preValue =
(newFilterDisplayValue.get(name) || 0) + INITIAL_FILTER_VALUE;
newFilterDisplayValue.set(name, preValue);
dispatch({
type: UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE,
payload: newFilterDisplayValue,
});
};
const isMoreButtonAvilable = Boolean(
numberOfFilters && statusObj.length > numberOfFilters,
);
return ( return (
<> <>
{statusObj.length > 0 && (
<Search
value={searchFilter}
onChange={(e): void => setSearchFilter(e.target.value)}
style={{
padding: '0 3%',
}}
placeholder="Filter Values"
/>
)}
{statusObj {statusObj
.sort((a, b) => { .sort((a, b) => {
const countA = +status[a]; const countA = +status[a];
@ -28,6 +66,15 @@ function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
} }
return countA - countB; return countA - countB;
}) })
.filter((filter) => {
if (searchFilter.length === 0) {
return true;
}
return filter
.toLocaleLowerCase()
.includes(searchFilter.toLocaleLowerCase());
})
.filter((_, index) => index < numberOfFilters)
.map((e) => ( .map((e) => (
<CheckBoxComponent <CheckBoxComponent
key={e} key={e}
@ -38,6 +85,12 @@ function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
}} }}
/> />
))} ))}
{isMoreButtonAvilable && (
<Button onClick={onClickMoreHandler} type="link">
More
</Button>
)}
</> </>
); );
} }

View File

@ -1,11 +1,14 @@
/* eslint-disable react/no-unstable-nested-components */ import { Slider } from 'antd';
import { Input, Slider } from 'antd';
import { SliderRangeProps } from 'antd/lib/slider'; import { SliderRangeProps } from 'antd/lib/slider';
import getFilters from 'api/trace/getFilters'; import getFilters from 'api/trace/getFilters';
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
import useDebouncedFn from 'hooks/useDebouncedFunction'; import useDebouncedFn from 'hooks/useDebouncedFunction';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util'; import { getFilter, updateURL } from 'store/actions/trace/util';
@ -15,19 +18,8 @@ import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace'; import { TraceReducer } from 'types/reducer/trace';
import { Container, InputContainer, Text } from './styles'; import { Container, InputComponent, InputContainer, Text } from './styles';
import { getMs } from './util';
dayjs.extend(durationPlugin);
const getMs = (value: string): string => {
return parseFloat(
dayjs
.duration({
milliseconds: parseInt(value, 10) / 1000000,
})
.format('SSS'),
).toFixed(2);
};
function Duration(): JSX.Element { function Duration(): JSX.Element {
const { const {
@ -77,17 +69,18 @@ function Duration(): JSX.Element {
preLocalMinDuration.current = parseFloat(minDuration); preLocalMinDuration.current = parseFloat(minDuration);
} }
setPreMax(maxDuration); setPreMax(getMs(maxDuration));
setPreMin(minDuration); setPreMin(getMs(minDuration));
}, [getDuration]); }, [getDuration]);
const defaultValue = [parseFloat(preMin), parseFloat(preMax)];
const updatedUrl = async (min: number, max: number): Promise<void> => { const updatedUrl = async (min: number, max: number): Promise<void> => {
const preSelectedFilter = new Map(selectedFilter); const preSelectedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter); const preUserSelected = new Map(userSelectedFilter);
preSelectedFilter.set('duration', [String(max), String(min)]); preSelectedFilter.set('duration', [
String(max * 1000000),
String(min * 1000000),
]);
const response = await getFilters({ const response = await getFilters({
end: String(globalTime.maxTime), end: String(globalTime.maxTime),
@ -137,18 +130,18 @@ function Duration(): JSX.Element {
} }
}; };
const onRangeSliderHandler = (number: [number, number]): void => { const onRangeSliderHandler = (number: [string, string]): void => {
const [min, max] = number; const [min, max] = number;
setPreMin(min.toString()); setPreMin(min);
setPreMax(max.toString()); setPreMax(max);
}; };
const debouncedFunction = useDebouncedFn( const debouncedFunction = useDebouncedFn(
(min, max) => { (min, max) => {
updatedUrl(min as number, max as number); updatedUrl(min as number, max as number);
}, },
500, 1500,
undefined, undefined,
); );
@ -156,8 +149,8 @@ function Duration(): JSX.Element {
event, event,
) => { ) => {
const { value } = event.target; const { value } = event.target;
const min = parseFloat(preMin); const min = preMin;
const max = parseFloat(value) * 1000000; const max = value;
onRangeSliderHandler([min, max]); onRangeSliderHandler([min, max]);
debouncedFunction(min, max); debouncedFunction(min, max);
@ -167,8 +160,9 @@ function Duration(): JSX.Element {
event, event,
) => { ) => {
const { value } = event.target; const { value } = event.target;
const min = parseFloat(value) * 1000000; const min = value;
const max = parseFloat(preMax); const max = preMax;
onRangeSliderHandler([min, max]); onRangeSliderHandler([min, max]);
debouncedFunction(min, max); debouncedFunction(min, max);
}; };
@ -177,45 +171,48 @@ function Duration(): JSX.Element {
updatedUrl(min, max); updatedUrl(min, max);
}; };
const TipComponent = useCallback((value) => {
if (value === undefined) {
return <div />;
}
return <div>{`${getMs(value?.toString())}ms`}</div>;
}, []);
return ( return (
<div> <div>
<Container> <Container>
<InputContainer> <InputContainer>
<Text>Min</Text> <Text>Min</Text>
</InputContainer> </InputContainer>
<Input <InputComponent
addonAfter="ms" addonAfter="ms"
type="number"
onChange={onChangeMinHandler} onChange={onChangeMinHandler}
value={getMs(preMin)} value={preMin}
/> />
<InputContainer> <InputContainer>
<Text>Max</Text> <Text>Max</Text>
</InputContainer> </InputContainer>
<Input <InputComponent
addonAfter="ms" addonAfter="ms"
type="number"
onChange={onChangeMaxHandler} onChange={onChangeMaxHandler}
value={getMs(preMax)} value={preMax}
/> />
</Container> </Container>
<Container> <Container>
<Slider <Slider
defaultValue={[defaultValue[0], defaultValue[1]]} min={Number(getMs(String(preLocalMinDuration.current || 0)))}
min={parseFloat((preLocalMinDuration.current || 0).toString())} max={Number(getMs(String(preLocalMaxDuration.current || 0)))}
max={parseFloat((preLocalMaxDuration.current || 0).toString())}
range range
tipFormatter={(value): JSX.Element => { tipFormatter={TipComponent}
if (value === undefined) {
return <div />;
}
return <div>{`${getMs(value?.toString())}ms`}</div>;
}}
onChange={([min, max]): void => { onChange={([min, max]): void => {
onRangeSliderHandler([min, max]); onRangeSliderHandler([String(min), String(max)]);
}} }}
onAfterChange={onRangeHandler} onAfterChange={onRangeHandler}
value={[parseFloat(preMin), parseFloat(preMax)]} value={[Number(preMin), Number(preMax)]}
/> />
</Container> </Container>
</div> </div>

View File

@ -1,4 +1,4 @@
import { Typography } from 'antd'; import { Input, Typography } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
export const DurationText = styled.div` export const DurationText = styled.div`
@ -9,6 +9,19 @@ export const DurationText = styled.div`
flex-direction: column; flex-direction: column;
`; `;
export const InputComponent = styled(Input)`
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
`;
export const InputContainer = styled.div` export const InputContainer = styled.div`
width: 100%; width: 100%;
margin-top: 0.5rem; margin-top: 0.5rem;

View File

@ -0,0 +1,13 @@
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
dayjs.extend(durationPlugin);
export const getMs = (value: string): string =>
parseFloat(
dayjs
.duration({
milliseconds: parseInt(value, 10) / 1000000,
})
.format('SSS'),
).toFixed(2);

View File

@ -73,11 +73,24 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
<AutoComplete <AutoComplete
dropdownClassName="certain-category-search-dropdown" dropdownClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth={500} dropdownMatchSelectWidth={500}
style={{ width: 300 }} style={{ width: '100%' }}
options={options}
value={selectedKey} value={selectedKey}
onChange={(value): void => { allowClear
if (options && options.find((option) => option.value === value)) { showSearch
options={options?.map((e) => ({
label: e.label?.toString(),
value: e.value,
}))}
filterOption={(inputValue, option): boolean =>
option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
onChange={(e): void => setSelectedKey(e)}
onSelect={(value: unknown): void => {
if (
typeof value === 'string' &&
options &&
options.find((option) => option.value === value)
) {
setSelectedKey(value); setSelectedKey(value);
setLocalSelectedTags((tags) => [ setLocalSelectedTags((tags) => [
@ -89,8 +102,6 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
}, },
...tags.slice(index + 1, tags.length), ...tags.slice(index + 1, tags.length),
]); ]);
} else {
setSelectedKey('');
} }
}} }}
> >

View File

@ -1,13 +1,13 @@
import { Select } from 'antd'; import { Select } from 'antd';
import getTagValue from 'api/trace/getTagValue'; import getTagValue from 'api/trace/getTagValue';
import React from 'react'; import React, { useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace'; import { TraceReducer } from 'types/reducer/trace';
import { SelectComponent } from './styles'; import { AutoCompleteComponent } from './styles';
function TagValue(props: TagValueProps): JSX.Element { function TagValue(props: TagValueProps): JSX.Element {
const { tag, setLocalSelectedTags, index, tagKey } = props; const { tag, setLocalSelectedTags, index, tagKey } = props;
@ -16,6 +16,7 @@ function TagValue(props: TagValueProps): JSX.Element {
Operator: selectedOperator, Operator: selectedOperator,
Values: selectedValues, Values: selectedValues,
} = tag; } = tag;
const [localValue, setLocalValue] = useState<string>(selectedValues[0]);
const globalReducer = useSelector<AppState, GlobalReducer>( const globalReducer = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime, (state) => state.globalTime,
@ -34,22 +35,38 @@ function TagValue(props: TagValueProps): JSX.Element {
); );
return ( return (
<SelectComponent <AutoCompleteComponent
value={selectedValues[0]} options={data?.payload?.map((e) => ({
label: e.tagValues,
value: e.tagValues,
}))}
allowClear
defaultOpen
showSearch
filterOption={(inputValue, option): boolean =>
option?.label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
disabled={isLoading}
value={localValue}
onChange={(values): void => {
if (typeof values === 'string') {
setLocalValue(values);
}
}}
onSelect={(value: unknown): void => { onSelect={(value: unknown): void => {
if (typeof value === 'string') { if (typeof value === 'string') {
setLocalValue(value);
setLocalSelectedTags((tags) => [ setLocalSelectedTags((tags) => [
...tags.slice(0, index), ...tags.slice(0, index),
{ {
Key: selectedKey, Key: selectedKey,
Operator: selectedOperator, Operator: selectedOperator,
Values: [...selectedValues, value], Values: [value],
}, },
...tags.slice(index + 1, tags.length), ...tags.slice(index + 1, tags.length),
]); ]);
} }
}} }}
loading={isLoading || false}
> >
{data && {data &&
data.payload && data.payload &&
@ -58,7 +75,7 @@ function TagValue(props: TagValueProps): JSX.Element {
{suggestion.tagValues} {suggestion.tagValues}
</Select.Option> </Select.Option>
))} ))}
</SelectComponent> </AutoCompleteComponent>
); );
} }

View File

@ -1,4 +1,4 @@
import { Select, Space } from 'antd'; import { AutoComplete, Select, Space } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
export const SpaceComponent = styled(Space)` export const SpaceComponent = styled(Space)`
@ -9,18 +9,23 @@ export const SpaceComponent = styled(Space)`
export const SelectComponent = styled(Select)` export const SelectComponent = styled(Select)`
&&& { &&& {
min-width: 170px; width: 100%;
margin-right: 21.91px;
margin-left: 21.92px;
} }
`; `;
export const Container = styled.div` export const Container = styled(Space)`
&&& { &&& {
display: flex; display: flex;
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.ant-space-item:not(:last-child, :nth-child(2)) {
width: 100%;
}
.ant-space-item:nth-child(2) {
width: 50%;
}
`; `;
export const IconContainer = styled.div` export const IconContainer = styled.div`
@ -31,3 +36,9 @@ export const IconContainer = styled.div`
margin-left: 1.125rem; margin-left: 1.125rem;
`; `;
export const AutoCompleteComponent = styled(AutoComplete)`
&&& {
width: 100%;
}
`;

View File

@ -1,7 +1,7 @@
import { Space, Tabs, Typography } from 'antd'; import { Tabs, Tooltip, Typography } from 'antd';
import { StyledSpace } from 'components/Styled'; import { StyledSpace } from 'components/Styled';
import useThemeMode from 'hooks/useThemeMode'; import useThemeMode from 'hooks/useThemeMode';
import React from 'react'; import React, { useMemo } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem'; import { ITraceTree } from 'types/api/trace/getTraceItem';
import ErrorTag from './ErrorTag'; import ErrorTag from './ErrorTag';
@ -19,29 +19,38 @@ const { TabPane } = Tabs;
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element { function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
const { tree } = props; const { tree } = props;
const { isDarkMode } = useThemeMode(); const { isDarkMode } = useThemeMode();
const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]);
const OverLayComponentServiceName = useMemo(() => tree?.serviceName, [
tree?.serviceName,
]);
if (!tree) { if (!tree) {
return <div />; return <div />;
} }
const { name, tags, serviceName } = tree; const { tags } = tree;
return ( return (
<CardContainer> <CardContainer>
<StyledSpace <StyledSpace
styledclass={[styles.selectedSpanDetailsContainer]} styledclass={[styles.selectedSpanDetailsContainer, styles.overflow]}
direction="vertical" direction="vertical"
style={{ marginLeft: '0.5rem' }} style={{ marginLeft: '0.5rem' }}
> >
<strong> Details for selected Span </strong> <strong> Details for selected Span </strong>
<Space direction="vertical" size={2}>
<CustomTitle>Service</CustomTitle> <CustomTitle>Service</CustomTitle>
<CustomText>{serviceName}</CustomText> <Tooltip overlay={OverLayComponentServiceName}>
</Space> <CustomText ellipsis>{tree.serviceName}</CustomText>
<Space direction="vertical" size={2}> </Tooltip>
<CustomTitle>Operation</CustomTitle> <CustomTitle>Operation</CustomTitle>
<CustomText>{name}</CustomText> <Tooltip overlay={OverLayComponentName}>
</Space> <CustomText ellipsis>{tree.name}</CustomText>
</Tooltip>
</StyledSpace> </StyledSpace>
<Tabs defaultActiveKey="1"> <Tabs defaultActiveKey="1">
<TabPane tab="Tags" key="1"> <TabPane tab="Tags" key="1">
{tags.length !== 0 ? ( {tags.length !== 0 ? (

View File

@ -1,7 +1,7 @@
import { Typography } from 'antd'; import { Space, Typography } from 'antd';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
const { Text, Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
export const CustomTitle = styled(Title)` export const CustomTitle = styled(Title)`
&&& { &&& {
@ -9,7 +9,7 @@ export const CustomTitle = styled(Title)`
} }
`; `;
export const CustomText = styled(Text)` export const CustomText = styled(Paragraph)`
&&& { &&& {
color: #2d9cdb; color: #2d9cdb;
} }
@ -17,7 +17,6 @@ export const CustomText = styled(Text)`
export const CustomSubTitle = styled(Title)` export const CustomSubTitle = styled(Title)`
&&& { &&& {
/* color: #bdbdbd; */
font-size: 14px; font-size: 14px;
margin-bottom: 8px; margin-bottom: 8px;
} }
@ -44,6 +43,17 @@ export const CardContainer = styled.div`
width: 100%; width: 100%;
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
export const CustomSpace = styled(Space)`
&&& {
.ant-space-item {
width: 100%;
}
}
`; `;
const removeMargin = css` const removeMargin = css`
@ -60,9 +70,21 @@ const selectedSpanDetailsContainer = css`
const spanEventsTabsContainer = css` const spanEventsTabsContainer = css`
margin-top: 1rem; margin-top: 1rem;
`; `;
const overflow = css`
width: 95%;
> div.ant-space-item:nth-child(4) {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
export const styles = { export const styles = {
removeMargin, removeMargin,
removePadding, removePadding,
selectedSpanDetailsContainer, selectedSpanDetailsContainer,
spanEventsTabsContainer, spanEventsTabsContainer,
overflow,
}; };

View File

@ -2,12 +2,30 @@
exports[`loads and displays greeting 1`] = ` exports[`loads and displays greeting 1`] = `
<DocumentFragment> <DocumentFragment>
<div .c1 {
class="sc-gsDKAQ jFDWPs" position: absolute;
top: 0px;
left: NaN%;
width: Infinity%;
height: 10px;
margin: 1px 0;
background-color: hsl(282.9,100%,60.7%);
border-radius: 5px;
z-index: 1;
}
.c0 {
position: relative;
width: 100%;
height: 120px;
}
<div
class="c0"
height="0" height="0"
> >
<div <div
class="sc-bdvvtL fyFVjh" class="c1"
title="" title=""
width="Infinity" width="Infinity"
/> />

View File

@ -1,23 +1,45 @@
import { notification } from 'antd';
import get from 'api/alerts/get'; import get from 'api/alerts/get';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import ROUTES from 'constants/routes';
import EditRulesContainer from 'container/EditRules'; import EditRulesContainer from 'container/EditRules';
import React from 'react'; import history from 'lib/history';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useParams } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
function EditRules(): JSX.Element { function EditRules(): JSX.Element {
const { ruleId } = useParams<EditRulesParam>(); const { search } = useLocation();
const params = new URLSearchParams(search);
const ruleId = params.get('ruleId');
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
const { isLoading, data, isError } = useQuery(['ruleId', ruleId], { const { isLoading, data, isError } = useQuery(['ruleId', ruleId], {
queryFn: () => queryFn: () =>
get({ get({
id: parseInt(ruleId, 10), id: parseInt(ruleId || '', 10),
}), }),
enabled: isValidRuleId,
}); });
if (isError) { useEffect(() => {
if (!isValidRuleId) {
notification.error({
message: 'Rule Id is required',
});
history.replace(ROUTES.LIST_ALL_ALERT);
}
}, [isValidRuleId, ruleId]);
if (
(isError && !isValidRuleId) ||
ruleId == null ||
(data?.payload?.data === undefined && !isLoading)
) {
return <div>{data?.error || t('something_went_wrong')}</div>; return <div>{data?.error || t('something_went_wrong')}</div>;
} }
@ -28,8 +50,4 @@ function EditRules(): JSX.Element {
return <EditRulesContainer ruleId={ruleId} initialData={data.payload.data} />; return <EditRulesContainer ruleId={ruleId} initialData={data.payload.data} />;
} }
interface EditRulesParam {
ruleId: string;
}
export default EditRules; export default EditRules;

View File

@ -11,6 +11,7 @@ import {
UPDATE_SELECTED_TAGS, UPDATE_SELECTED_TAGS,
UPDATE_SPAN_ORDER, UPDATE_SPAN_ORDER,
UPDATE_SPAN_ORDER_PARAMS, UPDATE_SPAN_ORDER_PARAMS,
UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE,
UPDATE_SPANS_AGGREGATE, UPDATE_SPANS_AGGREGATE,
UPDATE_SPANS_AGGREGATE_PAGE_NUMBER, UPDATE_SPANS_AGGREGATE_PAGE_NUMBER,
UPDATE_SPANS_AGGREGATE_PAGE_SIZE, UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
@ -23,6 +24,8 @@ import {
} from 'types/actions/trace'; } from 'types/actions/trace';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
export const INITIAL_FILTER_VALUE = 8;
const initialValue: TraceReducer = { const initialValue: TraceReducer = {
filter: new Map(), filter: new Map(),
filterToFetchData: ['duration', 'status', 'serviceName'], filterToFetchData: ['duration', 'status', 'serviceName'],
@ -53,6 +56,17 @@ const initialValue: TraceReducer = {
loading: true, loading: true,
payload: { items: {} }, payload: { items: {} },
}, },
filterDisplayValue: new Map<TraceFilterEnum, number>([
['component', INITIAL_FILTER_VALUE],
['duration', INITIAL_FILTER_VALUE],
['httpCode', INITIAL_FILTER_VALUE],
['httpHost', INITIAL_FILTER_VALUE],
['httpMethod', INITIAL_FILTER_VALUE],
['httpUrl', INITIAL_FILTER_VALUE],
['operation', INITIAL_FILTER_VALUE],
['serviceName', INITIAL_FILTER_VALUE],
['status', INITIAL_FILTER_VALUE],
]),
}; };
const traceReducer = ( const traceReducer = (
@ -251,6 +265,13 @@ const traceReducer = (
}; };
} }
case UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE: {
return {
...state,
filterDisplayValue: action.payload,
};
}
default: default:
return state; return state;
} }

View File

@ -31,6 +31,8 @@ export const UPDATE_SPANS_AGGREGATE_PAGE_NUMBER =
export const UPDATE_SPANS_AGGREGATE_PAGE_SIZE = export const UPDATE_SPANS_AGGREGATE_PAGE_SIZE =
'UPDATE_SPANS_AGGREGATE_PAGE_SIZE'; 'UPDATE_SPANS_AGGREGATE_PAGE_SIZE';
export const UPDATE_SPAN_ORDER_PARAMS = 'UPDATE_SPAN_ORDER_PARAMS'; export const UPDATE_SPAN_ORDER_PARAMS = 'UPDATE_SPAN_ORDER_PARAMS';
export const UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE =
'UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE';
export interface UpdateFilter { export interface UpdateFilter {
type: typeof UPDATE_TRACE_FILTER; type: typeof UPDATE_TRACE_FILTER;
@ -187,6 +189,11 @@ export interface UpdateSpanParams {
}; };
} }
export interface UpdateTraceFilterDisplayValue {
type: typeof UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE;
payload: TraceReducer['filterDisplayValue'];
}
export type TraceActions = export type TraceActions =
| UpdateFilter | UpdateFilter
| GetTraceFilter | GetTraceFilter
@ -208,4 +215,5 @@ export type TraceActions =
| UpdateSpanOrder | UpdateSpanOrder
| UpdateSpansAggregatePageNumber | UpdateSpansAggregatePageNumber
| UpdateSpanSize | UpdateSpanSize
| UpdateSpanParams; | UpdateSpanParams
| UpdateTraceFilterDisplayValue;

View File

@ -32,6 +32,7 @@ export interface TraceReducer {
payload: PayloadProps; payload: PayloadProps;
}; };
yAxisUnit: string | undefined; yAxisUnit: string | undefined;
filterDisplayValue: Map<TraceFilterEnum, number>;
} }
interface SpansAggregateData { interface SpansAggregateData {

View File

@ -35,6 +35,7 @@
"playwright.config.ts", "playwright.config.ts",
"./commitlint.config.js", "./commitlint.config.js",
"./webpack.config.js", "./webpack.config.js",
"./webpack.config.prod.js" "./webpack.config.prod.js",
"./jest.setup.ts"
] ]
} }

View File

@ -8128,6 +8128,13 @@ jest-snapshot@^27.5.1:
pretty-format "^27.5.1" pretty-format "^27.5.1"
semver "^7.3.2" semver "^7.3.2"
jest-styled-components@^7.0.8:
version "7.0.8"
resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-7.0.8.tgz#9ea3b43f002de060b4638fde3b422d14b3e3ec9f"
integrity sha512-0KE54d0yIzKcvtOv8eikyjG3rFRtKYUyQovaoha3nondtZzXYGB3bhsvYgEegU08Iry0ndWx2+g9f5ZzD4I+0Q==
dependencies:
css "^3.0.0"
jest-util@^26.6.2: jest-util@^26.6.2:
version "26.6.2" version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1"

View File

@ -592,21 +592,45 @@ func (r *ClickHouseReader) GetRulesFromDB() (*[]model.RuleResponseItem, *model.A
func (r *ClickHouseReader) GetRule(id string) (*model.RuleResponseItem, *model.ApiError) { func (r *ClickHouseReader) GetRule(id string) (*model.RuleResponseItem, *model.ApiError) {
idInt, _ := strconv.Atoi(id) idInt, err := strconv.Atoi(id)
if err != nil {
zap.S().Debug("Error in parsing param: ", err)
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
rule := &model.RuleResponseItem{} rule := &model.RuleResponseItem{}
query := fmt.Sprintf("SELECT id, updated_at, data FROM rules WHERE id=%d", idInt) query := "SELECT id, updated_at, data FROM rules WHERE id=?"
rows, err := r.localDB.Query(query, idInt)
err := r.localDB.Get(rule, query)
zap.S().Info(query)
if err != nil { if err != nil {
zap.S().Debug("Error in processing sql query: ", err) zap.S().Debug("Error in processing sql query: ", err)
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
} }
count := 0
// iterate over each row
for rows.Next() {
err = rows.Scan(&rule.Id, &rule.UpdatedAt, &rule.Data)
if err != nil {
zap.S().Debug(err)
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
count += 1
}
if count == 0 {
err = fmt.Errorf("no rule with id %d found", idInt)
zap.S().Debug(err)
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: err}
}
if count > 1 {
err = fmt.Errorf("multiple rules with id %d found", idInt)
zap.S().Debug(err)
return nil, &model.ApiError{Typ: model.ErrorConflict, Err: err}
}
return rule, nil return rule, nil
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
<?xml version="1.0"?>
<clickhouse>
<storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<endpoint>http://172.17.0.1:9100/test//</endpoint>
<access_key_id>ash</access_key_id>
<secret_access_key>password</secret_access_key>
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration>
</clickhouse>

View File

@ -0,0 +1,123 @@
<?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

@ -1,99 +0,0 @@
version: "2.4"
services:
clickhouse:
image: altinity/clickhouse-server:21.12.3.32.altinitydev.arm
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
restart: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
interval: 30s
timeout: 5s
retries: 3
alertmanager:
image: signoz/alertmanager:0.23.0-0.1
depends_on:
- query-service
restart: on-failure
command:
- --queryService.url=http://query-service:8085
- --storage.path=/data
query-service:
image: signoz/query-service:latest
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
- ./data:/var/lib/signoz
ports:
- "8180:8080"
environment:
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
clickhouse:
condition: service_healthy
otel-collector:
image: signoz/otelcontribcol:0.45.1-0.3
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317" # OTLP GRPC receiver
mem_limit: 2000m
restart: always
depends_on:
clickhouse:
condition: service_healthy
otel-collector-metrics:
image: signoz/otelcontribcol:0.45.1-0.3
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
depends_on:
clickhouse:
condition: service_healthy
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
container_name: load-hotrod
hostname: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../../../../deploy/docker/common/locust-scripts:/locust

View File

@ -2,9 +2,11 @@ version: "2.4"
services: services:
clickhouse: clickhouse:
image: yandex/clickhouse-server:21.12.3.32 image: clickhouse/clickhouse-server:22.4.5-alpine
volumes: volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
- ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
restart: on-failure restart: on-failure
logging: logging:
options: options: