mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 18:56:02 +08:00
commit
d170515d4d
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ package.json
|
|||||||
deploy/docker/environment_tiny/common_test
|
deploy/docker/environment_tiny/common_test
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
frontend/.pnp
|
frontend/.pnp
|
||||||
|
frontend/i18n-translations-hash.json
|
||||||
*.pnp.js
|
*.pnp.js
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
|
20
Makefile
20
Makefile
@ -13,6 +13,8 @@ FRONTEND_DIRECTORY ?= frontend
|
|||||||
QUERY_SERVICE_DIRECTORY ?= pkg/query-service
|
QUERY_SERVICE_DIRECTORY ?= pkg/query-service
|
||||||
STANDALONE_DIRECTORY ?= deploy/docker/clickhouse-setup
|
STANDALONE_DIRECTORY ?= deploy/docker/clickhouse-setup
|
||||||
SWARM_DIRECTORY ?= deploy/docker-swarm/clickhouse-setup
|
SWARM_DIRECTORY ?= deploy/docker-swarm/clickhouse-setup
|
||||||
|
LOCAL_GOOS ?= $(shell go env GOOS)
|
||||||
|
LOCAL_GOARCH ?= $(shell go env GOARCH)
|
||||||
|
|
||||||
REPONAME ?= signoz
|
REPONAME ?= signoz
|
||||||
DOCKER_TAG ?= latest
|
DOCKER_TAG ?= latest
|
||||||
@ -79,11 +81,25 @@ dev-setup:
|
|||||||
@echo "--> Local Setup completed"
|
@echo "--> Local Setup completed"
|
||||||
@echo "------------------"
|
@echo "------------------"
|
||||||
|
|
||||||
|
run-local:
|
||||||
|
@LOCAL_GOOS=$(LOCAL_GOOS) LOCAL_GOARCH=$(LOCAL_GOARCH) docker-compose -f \
|
||||||
|
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \
|
||||||
|
up --build -d
|
||||||
|
|
||||||
|
down-local:
|
||||||
|
@docker-compose -f \
|
||||||
|
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \
|
||||||
|
down -v
|
||||||
|
|
||||||
run-x86:
|
run-x86:
|
||||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up -d
|
@docker-compose -f \
|
||||||
|
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \
|
||||||
|
up --build -d
|
||||||
|
|
||||||
down-x86:
|
down-x86:
|
||||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v
|
@docker-compose -f \
|
||||||
|
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \
|
||||||
|
down -v
|
||||||
|
|
||||||
clear-standalone-data:
|
clear-standalone-data:
|
||||||
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
|
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
|
||||||
|
@ -40,7 +40,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.10.2
|
image: signoz/query-service:0.11.0
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@ -51,10 +51,12 @@ services:
|
|||||||
- ./data/signoz/:/var/lib/signoz/
|
- ./data/signoz/:/var/lib/signoz/
|
||||||
environment:
|
environment:
|
||||||
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
|
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
|
||||||
|
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
|
||||||
- STORAGE=clickhouse
|
- STORAGE=clickhouse
|
||||||
- GODEBUG=netdns=go
|
- GODEBUG=netdns=go
|
||||||
- TELEMETRY_ENABLED=true
|
- TELEMETRY_ENABLED=true
|
||||||
- DEPLOYMENT_TYPE=docker-swarm
|
- DEPLOYMENT_TYPE=docker-swarm
|
||||||
|
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
||||||
@ -68,7 +70,7 @@ services:
|
|||||||
- clickhouse
|
- clickhouse
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.10.2
|
image: signoz/frontend:0.11.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@ -81,10 +83,14 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/otelcontribcol:0.45.1-1.3
|
image: signoz-otel-collector:0.55.0
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
|
environment:
|
||||||
|
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||||
ports:
|
ports:
|
||||||
# - "1777:1777" # pprof extension
|
# - "1777:1777" # pprof extension
|
||||||
- "4317:4317" # OTLP gRPC receiver
|
- "4317:4317" # OTLP gRPC receiver
|
||||||
@ -97,21 +103,15 @@ services:
|
|||||||
# - "14268:14268" # Jaeger thrift HTTP
|
# - "14268:14268" # Jaeger thrift HTTP
|
||||||
# - "55678:55678" # OpenCensus receiver
|
# - "55678:55678" # OpenCensus receiver
|
||||||
# - "55679:55679" # zPages extension
|
# - "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:
|
deploy:
|
||||||
mode: replicated
|
mode: global
|
||||||
replicas: 3
|
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 2000m
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- clickhouse
|
- clickhouse
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz/otelcontribcol:0.45.1-1.3
|
image: signoz-otel-collector:0.55.0
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -1,4 +1,29 @@
|
|||||||
receivers:
|
receivers:
|
||||||
|
filelog/dockercontainers:
|
||||||
|
include: [ "/var/lib/docker/containers/*/*.log" ]
|
||||||
|
start_at: end
|
||||||
|
include_file_path: true
|
||||||
|
include_file_name: false
|
||||||
|
operators:
|
||||||
|
- type: json_parser
|
||||||
|
id: parser-docker
|
||||||
|
output: extract_metadata_from_filepath
|
||||||
|
timestamp:
|
||||||
|
parse_from: attributes.time
|
||||||
|
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
||||||
|
- type: regex_parser
|
||||||
|
id: extract_metadata_from_filepath
|
||||||
|
regex: '^.*containers/(?P<container_id>[^_]+)/.*log$'
|
||||||
|
parse_from: attributes["log.file.path"]
|
||||||
|
output: parse_body
|
||||||
|
- type: move
|
||||||
|
id: parse_body
|
||||||
|
from: attributes.log
|
||||||
|
to: body
|
||||||
|
output: time
|
||||||
|
- type: remove
|
||||||
|
id: time
|
||||||
|
field: attributes.time
|
||||||
opencensus:
|
opencensus:
|
||||||
endpoint: 0.0.0.0:55678
|
endpoint: 0.0.0.0:55678
|
||||||
otlp/spanmetrics:
|
otlp/spanmetrics:
|
||||||
@ -76,6 +101,16 @@ exporters:
|
|||||||
prometheus:
|
prometheus:
|
||||||
endpoint: 0.0.0.0:8889
|
endpoint: 0.0.0.0:8889
|
||||||
# logging: {}
|
# logging: {}
|
||||||
|
clickhouselogsexporter:
|
||||||
|
dsn: tcp://clickhouse:9000/
|
||||||
|
timeout: 5s
|
||||||
|
sending_queue:
|
||||||
|
queue_size: 100
|
||||||
|
retry_on_failure:
|
||||||
|
enabled: true
|
||||||
|
initial_interval: 5s
|
||||||
|
max_interval: 30s
|
||||||
|
max_elapsed_time: 300s
|
||||||
|
|
||||||
extensions:
|
extensions:
|
||||||
health_check:
|
health_check:
|
||||||
@ -106,3 +141,7 @@ service:
|
|||||||
metrics/spanmetrics:
|
metrics/spanmetrics:
|
||||||
receivers: [otlp/spanmetrics]
|
receivers: [otlp/spanmetrics]
|
||||||
exporters: [prometheus]
|
exporters: [prometheus]
|
||||||
|
logs:
|
||||||
|
receivers: [otlp, filelog/dockercontainers]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [clickhouselogsexporter]
|
||||||
|
108
deploy/docker/clickhouse-setup/docker-compose-core.yaml
Normal file
108
deploy/docker/clickhouse-setup/docker-compose-core.yaml
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
version: "2.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
clickhouse:
|
||||||
|
image: clickhouse/clickhouse-server:22.4.5-alpine
|
||||||
|
container_name: clickhouse
|
||||||
|
# ports:
|
||||||
|
# - "9000:9000"
|
||||||
|
# - "8123:8123"
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- ./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/
|
||||||
|
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:
|
||||||
|
container_name: alertmanager
|
||||||
|
image: signoz/alertmanager:0.23.0-0.2
|
||||||
|
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`
|
||||||
|
otel-collector:
|
||||||
|
container_name: otel-collector
|
||||||
|
image: signoz/signoz-otel-collector:0.55.0
|
||||||
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
|
# user: root # required for reading docker container logs
|
||||||
|
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
|
||||||
|
# - "8888:8888" # OtelCollector internal metrics
|
||||||
|
# - "8889:8889" # signoz spanmetrics exposed by the agent
|
||||||
|
# - "9411:9411" # Zipkin port
|
||||||
|
# - "13133:13133" # health check extension
|
||||||
|
# - "14250:14250" # Jaeger gRPC
|
||||||
|
# - "14268:14268" # Jaeger thrift HTTP
|
||||||
|
# - "55678:55678" # OpenCensus receiver
|
||||||
|
# - "55679:55679" # zPages extension
|
||||||
|
restart: on-failure
|
||||||
|
depends_on:
|
||||||
|
clickhouse:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
otel-collector-metrics:
|
||||||
|
container_name: otel-collector-metrics
|
||||||
|
image: signoz/signoz-otel-collector:0.55.0
|
||||||
|
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:
|
||||||
|
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
|
55
deploy/docker/clickhouse-setup/docker-compose-local.yaml
Normal file
55
deploy/docker/clickhouse-setup/docker-compose-local.yaml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
version: "2.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
query-service:
|
||||||
|
hostname: query-service
|
||||||
|
build:
|
||||||
|
context: "../../../pkg/query-service"
|
||||||
|
dockerfile: "./Dockerfile"
|
||||||
|
args:
|
||||||
|
LDFLAGS: ""
|
||||||
|
TARGETPLATFORM: "${LOCAL_GOOS}/${LOCAL_GOARCH}"
|
||||||
|
container_name: query-service
|
||||||
|
environment:
|
||||||
|
- ClickHouseUrl=tcp://clickhouse:9000
|
||||||
|
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
|
||||||
|
- STORAGE=clickhouse
|
||||||
|
- GODEBUG=netdns=go
|
||||||
|
- TELEMETRY_ENABLED=true
|
||||||
|
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
|
||||||
|
volumes:
|
||||||
|
- ./prometheus.yml:/root/config/prometheus.yml
|
||||||
|
- ../dashboards:/root/config/dashboards
|
||||||
|
- ./data/signoz/:/var/lib/signoz/
|
||||||
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
|
ports:
|
||||||
|
- "6060:6060"
|
||||||
|
- "8080:8080"
|
||||||
|
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:
|
||||||
|
build:
|
||||||
|
context: "../../../frontend"
|
||||||
|
dockerfile: "./Dockerfile"
|
||||||
|
args:
|
||||||
|
TARGETOS: "${LOCAL_GOOS}"
|
||||||
|
TARGETPLATFORM: "${LOCAL_GOARCH}"
|
||||||
|
container_name: frontend
|
||||||
|
environment:
|
||||||
|
- FRONTEND_API_ENDPOINT=http://query-service:8080
|
||||||
|
restart: on-failure
|
||||||
|
depends_on:
|
||||||
|
- alertmanager
|
||||||
|
- query-service
|
||||||
|
ports:
|
||||||
|
- "3301:3301"
|
||||||
|
volumes:
|
||||||
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
43
deploy/docker/clickhouse-setup/docker-compose-prod.yaml
Normal file
43
deploy/docker/clickhouse-setup/docker-compose-prod.yaml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
version: "2.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
query-service:
|
||||||
|
image: signoz/query-service:0.11.0
|
||||||
|
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
|
||||||
|
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
|
||||||
|
- STORAGE=clickhouse
|
||||||
|
- GODEBUG=netdns=go
|
||||||
|
- TELEMETRY_ENABLED=true
|
||||||
|
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||||
|
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
|
||||||
|
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.11.0
|
||||||
|
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
|
@ -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`
|
# 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:
|
query-service:
|
||||||
image: signoz/query-service:0.10.2
|
image: signoz/query-service:0.11.0
|
||||||
container_name: query-service
|
container_name: query-service
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
@ -51,6 +51,8 @@ services:
|
|||||||
- ./data/signoz/:/var/lib/signoz/
|
- ./data/signoz/:/var/lib/signoz/
|
||||||
environment:
|
environment:
|
||||||
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
|
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
|
||||||
|
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
|
||||||
|
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
|
||||||
- STORAGE=clickhouse
|
- STORAGE=clickhouse
|
||||||
- GODEBUG=netdns=go
|
- GODEBUG=netdns=go
|
||||||
- TELEMETRY_ENABLED=true
|
- TELEMETRY_ENABLED=true
|
||||||
@ -66,7 +68,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.10.2
|
image: signoz/frontend:0.11.0
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -78,10 +80,12 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/otelcontribcol:0.45.1-1.3
|
image: signoz/signoz-otel-collector:0.55.0
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
environment:
|
environment:
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||||
ports:
|
ports:
|
||||||
@ -96,14 +100,13 @@ services:
|
|||||||
# - "14268:14268" # Jaeger thrift HTTP
|
# - "14268:14268" # Jaeger thrift HTTP
|
||||||
# - "55678:55678" # OpenCensus receiver
|
# - "55678:55678" # OpenCensus receiver
|
||||||
# - "55679:55679" # zPages extension
|
# - "55679:55679" # zPages extension
|
||||||
mem_limit: 2000m
|
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
clickhouse:
|
clickhouse:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz/otelcontribcol:0.45.1-1.3
|
image: signoz/signoz-otel-collector:0.55.0
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -1,4 +1,29 @@
|
|||||||
receivers:
|
receivers:
|
||||||
|
filelog/dockercontainers:
|
||||||
|
include: [ "/var/lib/docker/containers/*/*.log" ]
|
||||||
|
start_at: end
|
||||||
|
include_file_path: true
|
||||||
|
include_file_name: false
|
||||||
|
operators:
|
||||||
|
- type: json_parser
|
||||||
|
id: parser-docker
|
||||||
|
output: extract_metadata_from_filepath
|
||||||
|
timestamp:
|
||||||
|
parse_from: attributes.time
|
||||||
|
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
||||||
|
- type: regex_parser
|
||||||
|
id: extract_metadata_from_filepath
|
||||||
|
regex: '^.*containers/(?P<container_id>[^_]+)/.*log$'
|
||||||
|
parse_from: attributes["log.file.path"]
|
||||||
|
output: parse_body
|
||||||
|
- type: move
|
||||||
|
id: parse_body
|
||||||
|
from: attributes.log
|
||||||
|
to: body
|
||||||
|
output: time
|
||||||
|
- type: remove
|
||||||
|
id: time
|
||||||
|
field: attributes.time
|
||||||
opencensus:
|
opencensus:
|
||||||
endpoint: 0.0.0.0:55678
|
endpoint: 0.0.0.0:55678
|
||||||
otlp/spanmetrics:
|
otlp/spanmetrics:
|
||||||
@ -85,6 +110,17 @@ exporters:
|
|||||||
endpoint: 0.0.0.0:8889
|
endpoint: 0.0.0.0:8889
|
||||||
# logging: {}
|
# logging: {}
|
||||||
|
|
||||||
|
clickhouselogsexporter:
|
||||||
|
dsn: tcp://clickhouse:9000/
|
||||||
|
timeout: 5s
|
||||||
|
sending_queue:
|
||||||
|
queue_size: 100
|
||||||
|
retry_on_failure:
|
||||||
|
enabled: true
|
||||||
|
initial_interval: 5s
|
||||||
|
max_interval: 30s
|
||||||
|
max_elapsed_time: 300s
|
||||||
|
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
metrics:
|
metrics:
|
||||||
@ -109,3 +145,7 @@ service:
|
|||||||
metrics/spanmetrics:
|
metrics/spanmetrics:
|
||||||
receivers: [otlp/spanmetrics]
|
receivers: [otlp/spanmetrics]
|
||||||
exporters: [prometheus]
|
exporters: [prometheus]
|
||||||
|
logs:
|
||||||
|
receivers: [otlp, filelog/dockercontainers]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [clickhouselogsexporter]
|
@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
*.typegen.ts
|
*.typegen.ts
|
||||||
|
i18-generate-hash.js
|
24
frontend/i18-generate-hash.js
Normal file
24
frontend/i18-generate-hash.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const glob = require('glob');
|
||||||
|
|
||||||
|
function generateChecksum(str, algorithm, encoding) {
|
||||||
|
return crypto
|
||||||
|
.createHash(algorithm || 'md5')
|
||||||
|
.update(str, 'utf8')
|
||||||
|
.digest(encoding || 'hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
glob.sync(`public/locales/**/*.json`).forEach(path => {
|
||||||
|
const [_, lang] = path.split('public/locales');
|
||||||
|
const content = fs.readFileSync(path, { encoding: 'utf-8' });
|
||||||
|
result[lang.replace('.json', '')] = generateChecksum(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync('./i18n-translations-hash.json', JSON.stringify(result));
|
@ -4,19 +4,20 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "webpack.config.js",
|
"main": "webpack.config.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env NODE_ENV=development webpack serve --progress",
|
"i18n:generate-hash": "node ./i18-generate-hash.js",
|
||||||
"build": "webpack --config=webpack.config.prod.js --progress",
|
"dev": "npm run i18n:generate-hash && cross-env NODE_ENV=development webpack serve --progress",
|
||||||
|
"build": "npm run i18n:generate-hash && webpack --config=webpack.config.prod.js --progress",
|
||||||
"prettify": "prettier --write .",
|
"prettify": "prettier --write .",
|
||||||
"lint": "eslint ./src",
|
"lint": "npm run i18n:generate-hash && eslint ./src",
|
||||||
"lint:fix": "eslint ./src --fix",
|
"lint:fix": "npm run i18n:generate-hash && eslint ./src --fix",
|
||||||
"jest": "jest",
|
"jest": "jest",
|
||||||
"jest:coverage": "jest --coverage",
|
"jest:coverage": "jest --coverage",
|
||||||
"jest:watch": "jest --watch",
|
"jest:watch": "jest --watch",
|
||||||
"postinstall": "is-ci || yarn husky:configure",
|
"postinstall": "is-ci || yarn husky:configure",
|
||||||
"playwright": "NODE_ENV=testing playwright test --config=./playwright.config.ts",
|
"playwright": "npm run i18n:generate-hash && NODE_ENV=testing playwright test --config=./playwright.config.ts",
|
||||||
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
|
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
|
||||||
"playwright:codegen:local":"playwright codegen http://localhost:3301",
|
"playwright:codegen:local": "playwright codegen http://localhost:3301",
|
||||||
"playwright:codegen:local:auth":"yarn playwright:codegen:local --load-storage=tests/auth.json",
|
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
|
||||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||||
"commitlint": "commitlint --edit $1"
|
"commitlint": "commitlint --edit $1"
|
||||||
},
|
},
|
||||||
@ -55,7 +56,9 @@
|
|||||||
"d3-tip": "^0.9.1",
|
"d3-tip": "^0.9.1",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.10.7",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
|
"event-source-polyfill": "1.0.31",
|
||||||
"file-loader": "6.1.1",
|
"file-loader": "6.1.1",
|
||||||
|
"flat": "^5.0.2",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"html-webpack-plugin": "5.1.0",
|
"html-webpack-plugin": "5.1.0",
|
||||||
"i18next": "^21.6.12",
|
"i18next": "^21.6.12",
|
||||||
@ -123,6 +126,8 @@
|
|||||||
"@types/copy-webpack-plugin": "^8.0.1",
|
"@types/copy-webpack-plugin": "^8.0.1",
|
||||||
"@types/d3": "^6.2.0",
|
"@types/d3": "^6.2.0",
|
||||||
"@types/d3-tip": "^3.5.5",
|
"@types/d3-tip": "^3.5.5",
|
||||||
|
"@types/event-source-polyfill": "^1.0.0",
|
||||||
|
"@types/flat": "^5.0.2",
|
||||||
"@types/jest": "^27.5.1",
|
"@types/jest": "^27.5.1",
|
||||||
"@types/lodash-es": "^4.17.4",
|
"@types/lodash-es": "^4.17.4",
|
||||||
"@types/mini-css-extract-plugin": "^2.5.1",
|
"@types/mini-css-extract-plugin": "^2.5.1",
|
||||||
|
@ -100,6 +100,10 @@ export const MySettings = Loadable(
|
|||||||
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
|
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const Logs = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "Logs" */ 'pages/Logs'),
|
||||||
|
);
|
||||||
|
|
||||||
export const Login = Loadable(
|
export const Login = Loadable(
|
||||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
InstrumentationPage,
|
InstrumentationPage,
|
||||||
ListAllALertsPage,
|
ListAllALertsPage,
|
||||||
Login,
|
Login,
|
||||||
|
Logs,
|
||||||
MySettings,
|
MySettings,
|
||||||
NewDashboardPage,
|
NewDashboardPage,
|
||||||
OrganizationSettings,
|
OrganizationSettings,
|
||||||
@ -193,6 +194,13 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'MY_SETTINGS',
|
key: 'MY_SETTINGS',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.LOGS,
|
||||||
|
exact: true,
|
||||||
|
component: Logs,
|
||||||
|
key: 'LOGS',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.LOGIN,
|
path: ROUTES.LOGIN,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
@ -3,6 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector';
|
|||||||
import Backend from 'i18next-http-backend';
|
import Backend from 'i18next-http-backend';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import cacheBursting from '../../i18n-translations-hash.json';
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
// load translation using http -> see /public/locales
|
// load translation using http -> see /public/locales
|
||||||
.use(Backend)
|
.use(Backend)
|
||||||
@ -17,7 +19,14 @@ i18n
|
|||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // not needed for react as it escapes by default
|
escapeValue: false, // not needed for react as it escapes by default
|
||||||
},
|
},
|
||||||
|
backend: {
|
||||||
|
loadPath: (language, namespace) => {
|
||||||
|
const ns = namespace[0];
|
||||||
|
const pathkey = `/${language}/${ns}`;
|
||||||
|
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
|
||||||
|
return `/locales/${language}/${namespace}.json?h=${hash}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
react: {
|
react: {
|
||||||
useSuspense: false,
|
useSuspense: false,
|
||||||
},
|
},
|
||||||
|
23
frontend/src/api/logs/AddToSelectedField.ts
Normal file
23
frontend/src/api/logs/AddToSelectedField.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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/logs/addToSelectedFields';
|
||||||
|
|
||||||
|
const AddToSelectedFields = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const data = await axios.post(`/logs/fields`, props);
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: '',
|
||||||
|
payload: data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddToSelectedFields;
|
26
frontend/src/api/logs/GetLogs.ts
Normal file
26
frontend/src/api/logs/GetLogs.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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/logs/getLogs';
|
||||||
|
|
||||||
|
const GetLogs = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const data = await axios.get(`/logs`, {
|
||||||
|
params: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: '',
|
||||||
|
payload: data.data.results,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GetLogs;
|
26
frontend/src/api/logs/GetLogsAggregate.ts
Normal file
26
frontend/src/api/logs/GetLogsAggregate.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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/logs/getLogsAggregate';
|
||||||
|
|
||||||
|
const GetLogsAggregate = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const data = await axios.get(`/logs/aggregate`, {
|
||||||
|
params: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: '',
|
||||||
|
payload: data.data.items,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GetLogsAggregate;
|
24
frontend/src/api/logs/GetSearchFields.ts
Normal file
24
frontend/src/api/logs/GetSearchFields.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PayloadProps } from 'types/api/logs/getSearchFields';
|
||||||
|
|
||||||
|
const GetSearchFields = async (): Promise<
|
||||||
|
SuccessResponse<PayloadProps> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const data = await axios.get(`/logs/fields`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: '',
|
||||||
|
payload: data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GetSearchFields;
|
23
frontend/src/api/logs/RemoveFromSelectedField.ts
Normal file
23
frontend/src/api/logs/RemoveFromSelectedField.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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/logs/addToSelectedFields';
|
||||||
|
|
||||||
|
const RemoveSelectedField = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const data = await axios.post(`/logs/fields`, props);
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: '',
|
||||||
|
payload: data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemoveSelectedField;
|
17
frontend/src/api/logs/livetail.ts
Normal file
17
frontend/src/api/logs/livetail.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import apiV1 from 'api/apiV1';
|
||||||
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||||
|
|
||||||
|
export const LiveTail = (queryParams: string): EventSourcePolyfill => {
|
||||||
|
const dict = {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return new EventSourcePolyfill(
|
||||||
|
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
|
||||||
|
dict,
|
||||||
|
);
|
||||||
|
};
|
@ -65,6 +65,7 @@ function Graph({
|
|||||||
yAxisUnit = 'short',
|
yAxisUnit = 'short',
|
||||||
forceReRender,
|
forceReRender,
|
||||||
staticLine,
|
staticLine,
|
||||||
|
containerHeight,
|
||||||
}: GraphProps): JSX.Element {
|
}: GraphProps): JSX.Element {
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||||
@ -241,7 +242,7 @@ function Graph({
|
|||||||
}, [buildChart, forceReRender]);
|
}, [buildChart, forceReRender]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '85%' }}>
|
<div style={{ height: containerHeight }}>
|
||||||
<canvas ref={chartRef} />
|
<canvas ref={chartRef} />
|
||||||
<LegendsContainer id={name} />
|
<LegendsContainer id={name} />
|
||||||
</div>
|
</div>
|
||||||
@ -259,6 +260,7 @@ interface GraphProps {
|
|||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
forceReRender?: boolean | null | number;
|
forceReRender?: boolean | null | number;
|
||||||
staticLine?: StaticLineProps | undefined;
|
staticLine?: StaticLineProps | undefined;
|
||||||
|
containerHeight?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StaticLineProps {
|
export interface StaticLineProps {
|
||||||
@ -285,5 +287,6 @@ Graph.defaultProps = {
|
|||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
forceReRender: undefined,
|
forceReRender: undefined,
|
||||||
staticLine: undefined,
|
staticLine: undefined,
|
||||||
|
containerHeight: '85%',
|
||||||
};
|
};
|
||||||
export default Graph;
|
export default Graph;
|
||||||
|
147
frontend/src/components/Logs/AddToQueryHOC.tsx
Normal file
147
frontend/src/components/Logs/AddToQueryHOC.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { Button, Popover } from 'antd';
|
||||||
|
import getStep from 'lib/getStep';
|
||||||
|
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||||
|
import React, { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { getLogs } from 'store/actions/logs/getLogs';
|
||||||
|
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { SET_SEARCH_QUERY_STRING, TOGGLE_LIVE_TAIL } from 'types/actions/logs';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
interface AddToQueryHOCProps {
|
||||||
|
fieldKey: string;
|
||||||
|
fieldValue: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||||
|
getLogsAggregate: (
|
||||||
|
props: Parameters<typeof getLogsAggregate>[0],
|
||||||
|
) => ReturnType<typeof getLogsAggregate>;
|
||||||
|
}
|
||||||
|
function AddToQueryHOC({
|
||||||
|
fieldKey,
|
||||||
|
fieldValue,
|
||||||
|
children,
|
||||||
|
getLogs,
|
||||||
|
getLogsAggregate,
|
||||||
|
}: AddToQueryHOCProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
searchFilter: { queryString },
|
||||||
|
logLinesPerPage,
|
||||||
|
idStart,
|
||||||
|
idEnd,
|
||||||
|
liveTail,
|
||||||
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
const generatedQuery = useMemo(
|
||||||
|
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
|
||||||
|
[fieldKey, fieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQueryAdd = useCallback(() => {
|
||||||
|
let updatedQueryString = queryString || '';
|
||||||
|
|
||||||
|
if (updatedQueryString.length === 0) {
|
||||||
|
updatedQueryString += `${generatedQuery}`;
|
||||||
|
} else {
|
||||||
|
updatedQueryString += ` AND ${generatedQuery}`;
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: SET_SEARCH_QUERY_STRING,
|
||||||
|
payload: updatedQueryString,
|
||||||
|
});
|
||||||
|
if (liveTail === 'STOPPED') {
|
||||||
|
getLogs({
|
||||||
|
q: updatedQueryString,
|
||||||
|
limit: logLinesPerPage,
|
||||||
|
orderBy: 'timestamp',
|
||||||
|
order: 'desc',
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
...(idStart ? { idGt: idStart } : {}),
|
||||||
|
...(idEnd ? { idLt: idEnd } : {}),
|
||||||
|
});
|
||||||
|
getLogsAggregate({
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
step: getStep({
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
}),
|
||||||
|
q: updatedQueryString,
|
||||||
|
...(idStart ? { idGt: idStart } : {}),
|
||||||
|
...(idEnd ? { idLt: idEnd } : {}),
|
||||||
|
});
|
||||||
|
} else if (liveTail === 'PLAYING') {
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: 'PAUSED',
|
||||||
|
});
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: liveTail,
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
dispatch,
|
||||||
|
generatedQuery,
|
||||||
|
getLogs,
|
||||||
|
idEnd,
|
||||||
|
idStart,
|
||||||
|
logLinesPerPage,
|
||||||
|
maxTime,
|
||||||
|
minTime,
|
||||||
|
queryString,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const popOverContent = (
|
||||||
|
<span style={{ fontSize: '0.9rem' }}>Add to query: {fieldKey}</span>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
onClick={handleQueryAdd}
|
||||||
|
>
|
||||||
|
<Popover placement="top" content={popOverContent}>
|
||||||
|
{children}
|
||||||
|
</Popover>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
getLogs: (
|
||||||
|
props: Parameters<typeof getLogs>[0],
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
getLogsAggregate: (
|
||||||
|
props: Parameters<typeof getLogsAggregate>[0],
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
getLogs: bindActionCreators(getLogs, dispatch),
|
||||||
|
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(memo(AddToQueryHOC));
|
12
frontend/src/components/Logs/CategoryHeading/index.tsx
Normal file
12
frontend/src/components/Logs/CategoryHeading/index.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { CategoryHeadingText } from './styles';
|
||||||
|
|
||||||
|
interface ICategoryHeadingProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
function CategoryHeading({ children }: ICategoryHeadingProps): JSX.Element {
|
||||||
|
return <CategoryHeadingText type="secondary">{children}</CategoryHeadingText>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryHeading;
|
6
frontend/src/components/Logs/CategoryHeading/styles.ts
Normal file
6
frontend/src/components/Logs/CategoryHeading/styles.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const CategoryHeadingText = styled(Typography.Text)`
|
||||||
|
font-size: 0.8rem;
|
||||||
|
`;
|
37
frontend/src/components/Logs/CopyClipboardHOC.tsx
Normal file
37
frontend/src/components/Logs/CopyClipboardHOC.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Popover } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
|
||||||
|
interface CopyClipboardHOCProps {
|
||||||
|
textToCopy: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
function CopyClipboardHOC({
|
||||||
|
textToCopy,
|
||||||
|
children,
|
||||||
|
}: CopyClipboardHOCProps): JSX.Element {
|
||||||
|
const [, setCopy] = useCopyToClipboard();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={(): void => setCopy(textToCopy)}
|
||||||
|
onKeyDown={(): void => setCopy(textToCopy)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
placement="top"
|
||||||
|
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Popover>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyClipboardHOC;
|
157
frontend/src/components/Logs/LogItem/index.tsx
Normal file
157
frontend/src/components/Logs/LogItem/index.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { blue, grey, orange } from '@ant-design/colors';
|
||||||
|
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Divider, Row, Typography } from 'antd';
|
||||||
|
import { map } from 'd3';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import AddToQueryHOC from '../AddToQueryHOC';
|
||||||
|
import CopyClipboardHOC from '../CopyClipboardHOC';
|
||||||
|
import { Container } from './styles';
|
||||||
|
import { isValidLogField } from './util';
|
||||||
|
|
||||||
|
interface LogFieldProps {
|
||||||
|
fieldKey: string;
|
||||||
|
fieldValue: string;
|
||||||
|
}
|
||||||
|
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Text type="secondary">{fieldKey}</Typography.Text>
|
||||||
|
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||||
|
<Typography.Text ellipsis>
|
||||||
|
{': '}
|
||||||
|
{fieldValue}
|
||||||
|
</Typography.Text>
|
||||||
|
</CopyClipboardHOC>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function LogSelectedField({
|
||||||
|
fieldKey = '',
|
||||||
|
fieldValue = '',
|
||||||
|
}: LogFieldProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddToQueryHOC fieldKey={fieldKey} fieldValue={fieldValue}>
|
||||||
|
<Typography.Text>
|
||||||
|
{`"`}
|
||||||
|
<span style={{ color: blue[4] }}>{fieldKey}</span>
|
||||||
|
{`"`}
|
||||||
|
</Typography.Text>
|
||||||
|
</AddToQueryHOC>
|
||||||
|
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||||
|
<Typography.Text ellipsis>
|
||||||
|
<span>
|
||||||
|
{': '}
|
||||||
|
{typeof fieldValue === 'string' && `"`}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: orange[6] }}>{fieldValue}</span>
|
||||||
|
{typeof fieldValue === 'string' && `"`}
|
||||||
|
</Typography.Text>
|
||||||
|
</CopyClipboardHOC>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogItemProps {
|
||||||
|
logData: ILog;
|
||||||
|
}
|
||||||
|
function LogItem({ logData }: LogItemProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
fields: { selected },
|
||||||
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||||
|
const [, setCopy] = useCopyToClipboard();
|
||||||
|
|
||||||
|
const handleDetailedView = useCallback(() => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_DETAILED_LOG_DATA,
|
||||||
|
payload: logData,
|
||||||
|
});
|
||||||
|
}, [dispatch, logData]);
|
||||||
|
|
||||||
|
const handleCopyJSON = (): void => {
|
||||||
|
setCopy(JSON.stringify(logData, null, 2));
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<div style={{ maxWidth: '100%' }}>
|
||||||
|
<div>
|
||||||
|
{'{'}
|
||||||
|
<div style={{ marginLeft: '0.5rem' }}>
|
||||||
|
<LogGeneralField
|
||||||
|
fieldKey="log"
|
||||||
|
fieldValue={flattenLogData.body as never}
|
||||||
|
/>
|
||||||
|
{flattenLogData.stream && (
|
||||||
|
<LogGeneralField
|
||||||
|
fieldKey="stream"
|
||||||
|
fieldValue={flattenLogData.stream as never}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<LogGeneralField
|
||||||
|
fieldKey="timestamp"
|
||||||
|
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{'}'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{map(selected, (field) => {
|
||||||
|
return isValidLogField(flattenLogData[field.name] as never) ? (
|
||||||
|
<LogSelectedField
|
||||||
|
key={field.name}
|
||||||
|
fieldKey={field.name}
|
||||||
|
fieldValue={flattenLogData[field.name] as never}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />
|
||||||
|
<Row>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
onClick={handleDetailedView}
|
||||||
|
style={{ color: blue[5], padding: 0, margin: 0 }}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<ExpandAltOutlined /> View Details
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
onClick={handleCopyJSON}
|
||||||
|
style={{ padding: 0, margin: 0, color: grey[1] }}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<CopyFilled /> Copy JSON
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogItem;
|
18
frontend/src/components/Logs/LogItem/styles.ts
Normal file
18
frontend/src/components/Logs/LogItem/styles.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Card } from 'antd';
|
||||||
|
import styled, { keyframes } from 'styled-components';
|
||||||
|
|
||||||
|
const fadeInAnimation = keyframes`
|
||||||
|
0% { opacity: 0; }
|
||||||
|
100% { opacity: 1;}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Container = styled(Card)`
|
||||||
|
width: 100% !important;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
}
|
||||||
|
animation-name: ${fadeInAnimation};
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
`;
|
1
frontend/src/components/Logs/LogItem/util.ts
Normal file
1
frontend/src/components/Logs/LogItem/util.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const isValidLogField = (value: never): boolean => value !== undefined;
|
@ -30,4 +30,5 @@ export const QueryBuilderQueryTemplate = {
|
|||||||
export const QueryBuilderFormulaTemplate = {
|
export const QueryBuilderFormulaTemplate = {
|
||||||
expression: '',
|
expression: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
};
|
};
|
||||||
|
@ -26,6 +26,7 @@ const ROUTES = {
|
|||||||
SOMETHING_WENT_WRONG: '/something-went-wrong',
|
SOMETHING_WENT_WRONG: '/something-went-wrong',
|
||||||
UN_AUTHORIZED: '/un-authorized',
|
UN_AUTHORIZED: '/un-authorized',
|
||||||
NOT_FOUND: '/not-found',
|
NOT_FOUND: '/not-found',
|
||||||
|
LOGS: '/logs',
|
||||||
HOME_PAGE: '/',
|
HOME_PAGE: '/',
|
||||||
PASSWORD_RESET: '/password-reset',
|
PASSWORD_RESET: '/password-reset',
|
||||||
};
|
};
|
||||||
|
@ -68,15 +68,20 @@ function QuerySection({
|
|||||||
const handleFormulaChange = ({
|
const handleFormulaChange = ({
|
||||||
formulaIndex,
|
formulaIndex,
|
||||||
expression,
|
expression,
|
||||||
|
legend,
|
||||||
toggleDisable,
|
toggleDisable,
|
||||||
toggleDelete,
|
toggleDelete,
|
||||||
}: IQueryBuilderFormulaHandleChange): void => {
|
}: IQueryBuilderFormulaHandleChange): void => {
|
||||||
const allFormulas = formulaQueries;
|
const allFormulas = formulaQueries;
|
||||||
const current = allFormulas[formulaIndex];
|
const current = allFormulas[formulaIndex];
|
||||||
if (expression) {
|
if (expression !== undefined) {
|
||||||
current.expression = expression;
|
current.expression = expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (legend !== undefined) {
|
||||||
|
current.legend = legend;
|
||||||
|
}
|
||||||
|
|
||||||
if (toggleDisable) {
|
if (toggleDisable) {
|
||||||
current.disabled = !current.disabled;
|
current.disabled = !current.disabled;
|
||||||
}
|
}
|
||||||
@ -179,6 +184,7 @@ function QuerySection({
|
|||||||
formulaOnly: true,
|
formulaOnly: true,
|
||||||
expression: 'A',
|
expression: 'A',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
setFormulaQueries({ ...formulas });
|
setFormulaQueries({ ...formulas });
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
Col,
|
Col,
|
||||||
Divider,
|
Divider,
|
||||||
Modal,
|
Modal,
|
||||||
@ -26,6 +28,7 @@ import {
|
|||||||
} from 'types/api/disks/getDisks';
|
} from 'types/api/disks/getDisks';
|
||||||
import { TTTLType } from 'types/api/settings/common';
|
import { TTTLType } from 'types/api/settings/common';
|
||||||
import {
|
import {
|
||||||
|
PayloadPropsLogs as GetRetentionPeriodLogsPayload,
|
||||||
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
|
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
|
||||||
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
|
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
|
||||||
} from 'types/api/settings/getRetention';
|
} from 'types/api/settings/getRetention';
|
||||||
@ -40,19 +43,25 @@ type NumberOrNull = number | null;
|
|||||||
function GeneralSettings({
|
function GeneralSettings({
|
||||||
metricsTtlValuesPayload,
|
metricsTtlValuesPayload,
|
||||||
tracesTtlValuesPayload,
|
tracesTtlValuesPayload,
|
||||||
|
logsTtlValuesPayload,
|
||||||
getAvailableDiskPayload,
|
getAvailableDiskPayload,
|
||||||
metricsTtlValuesRefetch,
|
metricsTtlValuesRefetch,
|
||||||
tracesTtlValuesRefetch,
|
tracesTtlValuesRefetch,
|
||||||
|
logsTtlValuesRefetch,
|
||||||
}: GeneralSettingsProps): JSX.Element {
|
}: GeneralSettingsProps): JSX.Element {
|
||||||
const { t } = useTranslation(['generalSettings']);
|
const { t } = useTranslation(['generalSettings']);
|
||||||
const [modalMetrics, setModalMetrics] = useState<boolean>(false);
|
const [modalMetrics, setModalMetrics] = useState<boolean>(false);
|
||||||
|
const [modalTraces, setModalTraces] = useState<boolean>(false);
|
||||||
|
const [modalLogs, setModalLogs] = useState<boolean>(false);
|
||||||
|
|
||||||
const [postApiLoadingMetrics, setPostApiLoadingMetrics] = useState<boolean>(
|
const [postApiLoadingMetrics, setPostApiLoadingMetrics] = useState<boolean>(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
const [postApiLoadingTraces, setPostApiLoadingTraces] = useState<boolean>(
|
const [postApiLoadingTraces, setPostApiLoadingTraces] = useState<boolean>(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
const [modalTraces, setModalTraces] = useState<boolean>(false);
|
const [postApiLoadingLogs, setPostApiLoadingLogs] = useState<boolean>(false);
|
||||||
|
|
||||||
const [availableDisks] = useState<IDiskType[]>(getAvailableDiskPayload);
|
const [availableDisks] = useState<IDiskType[]>(getAvailableDiskPayload);
|
||||||
|
|
||||||
const [metricsCurrentTTLValues, setMetricsCurrentTTLValues] = useState(
|
const [metricsCurrentTTLValues, setMetricsCurrentTTLValues] = useState(
|
||||||
@ -62,6 +71,10 @@ function GeneralSettings({
|
|||||||
tracesTtlValuesPayload,
|
tracesTtlValuesPayload,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [logsCurrentTTLValues, setLogsCurrentTTLValues] = useState(
|
||||||
|
logsTtlValuesPayload,
|
||||||
|
);
|
||||||
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const [setRetentionPermission] = useComponentPermission(
|
const [setRetentionPermission] = useComponentPermission(
|
||||||
@ -86,6 +99,15 @@ function GeneralSettings({
|
|||||||
setTracesS3RetentionPeriod,
|
setTracesS3RetentionPeriod,
|
||||||
] = useState<NumberOrNull>(null);
|
] = useState<NumberOrNull>(null);
|
||||||
|
|
||||||
|
const [
|
||||||
|
logsTotalRetentionPeriod,
|
||||||
|
setLogsTotalRetentionPeriod,
|
||||||
|
] = useState<NumberOrNull>(null);
|
||||||
|
const [
|
||||||
|
logsS3RetentionPeriod,
|
||||||
|
setLogsS3RetentionPeriod,
|
||||||
|
] = useState<NumberOrNull>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (metricsCurrentTTLValues) {
|
if (metricsCurrentTTLValues) {
|
||||||
setMetricsTotalRetentionPeriod(
|
setMetricsTotalRetentionPeriod(
|
||||||
@ -112,6 +134,17 @@ function GeneralSettings({
|
|||||||
}
|
}
|
||||||
}, [tracesCurrentTTLValues]);
|
}, [tracesCurrentTTLValues]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logsCurrentTTLValues) {
|
||||||
|
setLogsTotalRetentionPeriod(logsCurrentTTLValues.logs_ttl_duration_hrs);
|
||||||
|
setLogsS3RetentionPeriod(
|
||||||
|
logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||||
|
? logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [logsCurrentTTLValues]);
|
||||||
|
|
||||||
useInterval(
|
useInterval(
|
||||||
async (): Promise<void> => {
|
async (): Promise<void> => {
|
||||||
if (metricsTtlValuesPayload.status === 'pending') {
|
if (metricsTtlValuesPayload.status === 'pending') {
|
||||||
@ -130,13 +163,24 @@ function GeneralSettings({
|
|||||||
tracesTtlValuesPayload.status === 'pending' ? 1000 : null,
|
tracesTtlValuesPayload.status === 'pending' ? 1000 : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useInterval(
|
||||||
|
async (): Promise<void> => {
|
||||||
|
if (logsTtlValuesPayload.status === 'pending') {
|
||||||
|
logsTtlValuesRefetch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logsTtlValuesPayload.status === 'pending' ? 1000 : null,
|
||||||
|
);
|
||||||
|
|
||||||
const onModalToggleHandler = (type: TTTLType): void => {
|
const onModalToggleHandler = (type: TTTLType): void => {
|
||||||
if (type === 'metrics') setModalMetrics((modal) => !modal);
|
if (type === 'metrics') setModalMetrics((modal) => !modal);
|
||||||
if (type === 'traces') setModalTraces((modal) => !modal);
|
if (type === 'traces') setModalTraces((modal) => !modal);
|
||||||
|
if (type === 'logs') setModalLogs((modal) => !modal);
|
||||||
};
|
};
|
||||||
const onPostApiLoadingHandler = (type: TTTLType): void => {
|
const onPostApiLoadingHandler = (type: TTTLType): void => {
|
||||||
if (type === 'metrics') setPostApiLoadingMetrics((modal) => !modal);
|
if (type === 'metrics') setPostApiLoadingMetrics((modal) => !modal);
|
||||||
if (type === 'traces') setPostApiLoadingTraces((modal) => !modal);
|
if (type === 'traces') setPostApiLoadingTraces((modal) => !modal);
|
||||||
|
if (type === 'logs') setPostApiLoadingLogs((modal) => !modal);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClickSaveHandler = useCallback(
|
const onClickSaveHandler = useCallback(
|
||||||
@ -157,7 +201,13 @@ function GeneralSettings({
|
|||||||
[availableDisks],
|
[availableDisks],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isMetricsSaveDisabled, isTracesSaveDisabled, errorText] = useMemo((): [
|
const [
|
||||||
|
isMetricsSaveDisabled,
|
||||||
|
isTracesSaveDisabled,
|
||||||
|
isLogsSaveDisabled,
|
||||||
|
errorText,
|
||||||
|
] = useMemo((): [
|
||||||
|
boolean,
|
||||||
boolean,
|
boolean,
|
||||||
boolean,
|
boolean,
|
||||||
string,
|
string,
|
||||||
@ -174,6 +224,7 @@ function GeneralSettings({
|
|||||||
// Defaults to button not disabled and empty error message text.
|
// Defaults to button not disabled and empty error message text.
|
||||||
let isMetricsSaveDisabled = false;
|
let isMetricsSaveDisabled = false;
|
||||||
let isTracesSaveDisabled = false;
|
let isTracesSaveDisabled = false;
|
||||||
|
let isLogsSaveDisabled = false;
|
||||||
let errorText = '';
|
let errorText = '';
|
||||||
|
|
||||||
if (s3Enabled) {
|
if (s3Enabled) {
|
||||||
@ -189,18 +240,35 @@ function GeneralSettings({
|
|||||||
) {
|
) {
|
||||||
isTracesSaveDisabled = true;
|
isTracesSaveDisabled = true;
|
||||||
errorText = messages.compareError('traces');
|
errorText = messages.compareError('traces');
|
||||||
|
} else if (
|
||||||
|
(logsTotalRetentionPeriod || logsS3RetentionPeriod) &&
|
||||||
|
Number(logsTotalRetentionPeriod) <= Number(logsS3RetentionPeriod)
|
||||||
|
) {
|
||||||
|
isLogsSaveDisabled = true;
|
||||||
|
errorText = messages.compareError('logs');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) {
|
if (
|
||||||
|
!metricsTotalRetentionPeriod ||
|
||||||
|
!tracesTotalRetentionPeriod ||
|
||||||
|
!logsTotalRetentionPeriod
|
||||||
|
) {
|
||||||
isMetricsSaveDisabled = true;
|
isMetricsSaveDisabled = true;
|
||||||
isTracesSaveDisabled = true;
|
isTracesSaveDisabled = true;
|
||||||
if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) {
|
isLogsSaveDisabled = true;
|
||||||
errorText = messages.nullValueError('metrics and traces');
|
if (
|
||||||
|
!metricsTotalRetentionPeriod &&
|
||||||
|
!tracesTotalRetentionPeriod &&
|
||||||
|
!logsTotalRetentionPeriod
|
||||||
|
) {
|
||||||
|
errorText = messages.nullValueError('metrics, traces and logs');
|
||||||
} else if (!metricsTotalRetentionPeriod) {
|
} else if (!metricsTotalRetentionPeriod) {
|
||||||
errorText = messages.nullValueError('metrics');
|
errorText = messages.nullValueError('metrics');
|
||||||
} else if (!tracesTotalRetentionPeriod) {
|
} else if (!tracesTotalRetentionPeriod) {
|
||||||
errorText = messages.nullValueError('traces');
|
errorText = messages.nullValueError('traces');
|
||||||
|
} else if (!logsTotalRetentionPeriod) {
|
||||||
|
errorText = messages.nullValueError('logs');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@ -219,8 +287,23 @@ function GeneralSettings({
|
|||||||
)
|
)
|
||||||
isTracesSaveDisabled = true;
|
isTracesSaveDisabled = true;
|
||||||
|
|
||||||
return [isMetricsSaveDisabled, isTracesSaveDisabled, errorText];
|
if (
|
||||||
|
logsCurrentTTLValues.logs_ttl_duration_hrs === logsTotalRetentionPeriod &&
|
||||||
|
logsCurrentTTLValues.logs_move_ttl_duration_hrs === logsS3RetentionPeriod
|
||||||
|
)
|
||||||
|
isLogsSaveDisabled = true;
|
||||||
|
|
||||||
|
return [
|
||||||
|
isMetricsSaveDisabled,
|
||||||
|
isTracesSaveDisabled,
|
||||||
|
isLogsSaveDisabled,
|
||||||
|
errorText,
|
||||||
|
];
|
||||||
}, [
|
}, [
|
||||||
|
logsCurrentTTLValues.logs_move_ttl_duration_hrs,
|
||||||
|
logsCurrentTTLValues.logs_ttl_duration_hrs,
|
||||||
|
logsS3RetentionPeriod,
|
||||||
|
logsTotalRetentionPeriod,
|
||||||
metricsCurrentTTLValues.metrics_move_ttl_duration_hrs,
|
metricsCurrentTTLValues.metrics_move_ttl_duration_hrs,
|
||||||
metricsCurrentTTLValues?.metrics_ttl_duration_hrs,
|
metricsCurrentTTLValues?.metrics_ttl_duration_hrs,
|
||||||
metricsS3RetentionPeriod,
|
metricsS3RetentionPeriod,
|
||||||
@ -235,21 +318,36 @@ function GeneralSettings({
|
|||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const onOkHandler = async (type: TTTLType): Promise<void> => {
|
const onOkHandler = async (type: TTTLType): Promise<void> => {
|
||||||
|
let apiCallTotalRetention;
|
||||||
|
let apiCallS3Retention;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'metrics': {
|
||||||
|
apiCallTotalRetention = metricsTotalRetentionPeriod;
|
||||||
|
apiCallS3Retention = metricsS3RetentionPeriod;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'traces': {
|
||||||
|
apiCallTotalRetention = tracesTotalRetentionPeriod;
|
||||||
|
apiCallS3Retention = tracesS3RetentionPeriod;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'logs': {
|
||||||
|
apiCallTotalRetention = logsTotalRetentionPeriod;
|
||||||
|
apiCallS3Retention = logsS3RetentionPeriod;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
onPostApiLoadingHandler(type);
|
onPostApiLoadingHandler(type);
|
||||||
const setTTLResponse = await setRetentionApi({
|
const setTTLResponse = await setRetentionApi({
|
||||||
type,
|
type,
|
||||||
totalDuration: `${
|
totalDuration: `${apiCallTotalRetention || -1}h`,
|
||||||
(type === 'metrics'
|
|
||||||
? metricsTotalRetentionPeriod
|
|
||||||
: tracesTotalRetentionPeriod) || -1
|
|
||||||
}h`,
|
|
||||||
coldStorage: s3Enabled ? 's3' : null,
|
coldStorage: s3Enabled ? 's3' : null,
|
||||||
toColdDuration: `${
|
toColdDuration: `${apiCallS3Retention || -1}h`,
|
||||||
(type === 'metrics'
|
|
||||||
? metricsS3RetentionPeriod
|
|
||||||
: tracesS3RetentionPeriod) || -1
|
|
||||||
}h`,
|
|
||||||
});
|
});
|
||||||
let hasSetTTLFailed = false;
|
let hasSetTTLFailed = false;
|
||||||
if (setTTLResponse.statusCode === 409) {
|
if (setTTLResponse.statusCode === 409) {
|
||||||
@ -281,6 +379,15 @@ function GeneralSettings({
|
|||||||
traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1,
|
traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1,
|
||||||
status: '',
|
status: '',
|
||||||
});
|
});
|
||||||
|
} else if (type === 'logs') {
|
||||||
|
logsTtlValuesRefetch();
|
||||||
|
if (!hasSetTTLFailed)
|
||||||
|
// Updates the currentTTL Values in order to avoid pushing the same values.
|
||||||
|
setLogsCurrentTTLValues({
|
||||||
|
logs_ttl_duration_hrs: logsTotalRetentionPeriod || -1,
|
||||||
|
logs_move_ttl_duration_hrs: logsS3RetentionPeriod || -1,
|
||||||
|
status: '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({
|
notification.error({
|
||||||
@ -375,16 +482,64 @@ function GeneralSettings({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
].map((category, idx, renderArr): JSX.Element | null => {
|
{
|
||||||
|
name: 'Logs',
|
||||||
|
retentionFields: [
|
||||||
|
{
|
||||||
|
name: t('total_retention_period'),
|
||||||
|
value: logsTotalRetentionPeriod,
|
||||||
|
setValue: setLogsTotalRetentionPeriod,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('move_to_s3'),
|
||||||
|
value: logsS3RetentionPeriod,
|
||||||
|
setValue: setLogsS3RetentionPeriod,
|
||||||
|
hide: !s3Enabled,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
save: {
|
||||||
|
modal: modalLogs,
|
||||||
|
modalOpen: (): void => onClickSaveHandler('logs'),
|
||||||
|
apiLoading: postApiLoadingLogs,
|
||||||
|
saveButtonText:
|
||||||
|
logsTtlValuesPayload.status === 'pending' ? (
|
||||||
|
<span>
|
||||||
|
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
|
||||||
|
{t('retention_save_button.pending', { name: 'logs' })}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{t('retention_save_button.success')}</span>
|
||||||
|
),
|
||||||
|
isDisabled: logsTtlValuesPayload.status === 'pending' || isLogsSaveDisabled,
|
||||||
|
},
|
||||||
|
statusComponent: (
|
||||||
|
<StatusMessage
|
||||||
|
total_retention={logsTtlValuesPayload.expected_logs_ttl_duration_hrs}
|
||||||
|
status={logsTtlValuesPayload.status}
|
||||||
|
s3_retention={logsTtlValuesPayload.expected_logs_move_ttl_duration_hrs}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
].map((category): JSX.Element | null => {
|
||||||
if (
|
if (
|
||||||
Array.isArray(category.retentionFields) &&
|
Array.isArray(category.retentionFields) &&
|
||||||
category.retentionFields.length > 0
|
category.retentionFields.length > 0
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={category.name}>
|
<React.Fragment key={category.name}>
|
||||||
<Col xs={22} xl={11} key={category.name}>
|
<Col xs={22} xl={11} key={category.name} style={{ margin: '0.5rem' }}>
|
||||||
<Typography.Title level={3}>{category.name}</Typography.Title>
|
<Card style={{ height: '100%', minHeight: 300 }}>
|
||||||
|
<Typography.Title style={{ margin: 0 }} level={3}>
|
||||||
|
{category.name}
|
||||||
|
</Typography.Title>
|
||||||
|
<Divider
|
||||||
|
style={{
|
||||||
|
margin: '0.5rem 0',
|
||||||
|
padding: 0,
|
||||||
|
opacity: 0.5,
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{category.retentionFields.map((retentionField) => (
|
{category.retentionFields.map((retentionField) => (
|
||||||
<Retention
|
<Retention
|
||||||
key={retentionField.name}
|
key={retentionField.name}
|
||||||
@ -426,12 +581,8 @@ function GeneralSettings({
|
|||||||
})}
|
})}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
{idx < renderArr.length && (
|
|
||||||
<Col xs={0} xl={1} style={{ textAlign: 'center' }}>
|
|
||||||
<Divider type="vertical" dashed style={{ height: '100%' }} />
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -451,7 +602,7 @@ function GeneralSettings({
|
|||||||
{errorText && <ErrorText>{errorText}</ErrorText>}
|
{errorText && <ErrorText>{errorText}</ErrorText>}
|
||||||
</ErrorTextContainer>
|
</ErrorTextContainer>
|
||||||
|
|
||||||
<Row justify="space-around">{renderConfig}</Row>
|
<Row justify="start">{renderConfig}</Row>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -460,12 +611,16 @@ interface GeneralSettingsProps {
|
|||||||
getAvailableDiskPayload: GetDisksPayload;
|
getAvailableDiskPayload: GetDisksPayload;
|
||||||
metricsTtlValuesPayload: GetRetentionPeriodMetricsPayload;
|
metricsTtlValuesPayload: GetRetentionPeriodMetricsPayload;
|
||||||
tracesTtlValuesPayload: GetRetentionPeriodTracesPayload;
|
tracesTtlValuesPayload: GetRetentionPeriodTracesPayload;
|
||||||
|
logsTtlValuesPayload: GetRetentionPeriodLogsPayload;
|
||||||
metricsTtlValuesRefetch: UseQueryResult<
|
metricsTtlValuesRefetch: UseQueryResult<
|
||||||
ErrorResponse | SuccessResponse<GetRetentionPeriodMetricsPayload>
|
ErrorResponse | SuccessResponse<GetRetentionPeriodMetricsPayload>
|
||||||
>['refetch'];
|
>['refetch'];
|
||||||
tracesTtlValuesRefetch: UseQueryResult<
|
tracesTtlValuesRefetch: UseQueryResult<
|
||||||
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
|
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
|
||||||
>['refetch'];
|
>['refetch'];
|
||||||
|
logsTtlValuesRefetch: UseQueryResult<
|
||||||
|
ErrorResponse | SuccessResponse<GetRetentionPeriodLogsPayload>
|
||||||
|
>['refetch'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GeneralSettings;
|
export default GeneralSettings;
|
||||||
|
@ -84,10 +84,10 @@ function Retention({
|
|||||||
return (
|
return (
|
||||||
<RetentionContainer>
|
<RetentionContainer>
|
||||||
<Row justify="space-between">
|
<Row justify="space-between">
|
||||||
<Col flex={1} style={{ display: 'flex' }}>
|
<Col span={12} style={{ display: 'flex' }}>
|
||||||
<RetentionFieldLabel>{text}</RetentionFieldLabel>
|
<RetentionFieldLabel>{text}</RetentionFieldLabel>
|
||||||
</Col>
|
</Col>
|
||||||
<Col flex="150px">
|
<Row justify="end">
|
||||||
<RetentionFieldInputContainer>
|
<RetentionFieldInputContainer>
|
||||||
<Input
|
<Input
|
||||||
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
|
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
|
||||||
@ -102,7 +102,7 @@ function Retention({
|
|||||||
{menuItems}
|
{menuItems}
|
||||||
</Select>
|
</Select>
|
||||||
</RetentionFieldInputContainer>
|
</RetentionFieldInputContainer>
|
||||||
</Col>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
</RetentionContainer>
|
</RetentionContainer>
|
||||||
);
|
);
|
||||||
|
@ -21,6 +21,7 @@ function GeneralSettings(): JSX.Element {
|
|||||||
const [
|
const [
|
||||||
getRetentionPeriodMetricsApiResponse,
|
getRetentionPeriodMetricsApiResponse,
|
||||||
getRetentionPeriodTracesApiResponse,
|
getRetentionPeriodTracesApiResponse,
|
||||||
|
getRetentionPeriodLogsApiResponse,
|
||||||
getDisksResponse,
|
getDisksResponse,
|
||||||
] = useQueries([
|
] = useQueries([
|
||||||
{
|
{
|
||||||
@ -33,6 +34,10 @@ function GeneralSettings(): JSX.Element {
|
|||||||
getRetentionPeriodApi('traces'),
|
getRetentionPeriodApi('traces'),
|
||||||
queryKey: 'getRetentionPeriodApiTraces',
|
queryKey: 'getRetentionPeriodApiTraces',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
queryFn: (): TRetentionAPIReturn<'logs'> => getRetentionPeriodApi('logs'),
|
||||||
|
queryKey: 'getRetentionPeriodApiLogs',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
queryFn: getDisks,
|
queryFn: getDisks,
|
||||||
queryKey: 'getDisks',
|
queryKey: 'getDisks',
|
||||||
@ -60,17 +65,27 @@ function GeneralSettings(): JSX.Element {
|
|||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Error State - When RetentionPeriodLogsApi or getDiskApi gets errored out.
|
||||||
|
if (getRetentionPeriodLogsApiResponse.isError || getDisksResponse.isError) {
|
||||||
|
return (
|
||||||
|
<Typography>
|
||||||
|
{getRetentionPeriodLogsApiResponse.data?.error ||
|
||||||
|
getDisksResponse.data?.error ||
|
||||||
|
t('something_went_wrong')}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Loading State - When Metrics, Traces and Disk API are in progress and the promise has not been resolved/reject.
|
// Loading State - When Metrics, Traces and Disk API are in progress and the promise has not been resolved/reject.
|
||||||
if (
|
if (
|
||||||
getRetentionPeriodMetricsApiResponse.isLoading ||
|
|
||||||
getDisksResponse.isLoading ||
|
getDisksResponse.isLoading ||
|
||||||
!getDisksResponse.data?.payload ||
|
!getDisksResponse.data?.payload ||
|
||||||
|
getRetentionPeriodMetricsApiResponse.isLoading ||
|
||||||
!getRetentionPeriodMetricsApiResponse.data?.payload ||
|
!getRetentionPeriodMetricsApiResponse.data?.payload ||
|
||||||
getRetentionPeriodTracesApiResponse.isLoading ||
|
getRetentionPeriodTracesApiResponse.isLoading ||
|
||||||
getDisksResponse.isLoading ||
|
!getRetentionPeriodTracesApiResponse.data?.payload ||
|
||||||
!getDisksResponse.data?.payload ||
|
getRetentionPeriodLogsApiResponse.isLoading ||
|
||||||
!getRetentionPeriodTracesApiResponse.data?.payload
|
!getRetentionPeriodLogsApiResponse.data?.payload
|
||||||
) {
|
) {
|
||||||
return <Spinner tip="Loading.." height="70vh" />;
|
return <Spinner tip="Loading.." height="70vh" />;
|
||||||
}
|
}
|
||||||
@ -83,6 +98,8 @@ function GeneralSettings(): JSX.Element {
|
|||||||
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
|
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
|
||||||
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
|
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
|
||||||
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
|
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
|
||||||
|
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.payload,
|
||||||
|
logsTtlValuesRefetch: getRetentionPeriodLogsApiResponse.refetch,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
121
frontend/src/container/LogControls/index.tsx
Normal file
121
frontend/src/container/LogControls/index.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
FastBackwardOutlined,
|
||||||
|
LeftOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button, Divider, Select } from 'antd';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { getLogs } from 'store/actions/logs/getLogs';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import {
|
||||||
|
GET_NEXT_LOG_LINES,
|
||||||
|
GET_PREVIOUS_LOG_LINES,
|
||||||
|
RESET_ID_START_AND_END,
|
||||||
|
SET_LOG_LINES_PER_PAGE,
|
||||||
|
} from 'types/actions/logs';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import { Container } from './styles';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
|
||||||
|
|
||||||
|
interface LogControlsProps {
|
||||||
|
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||||
|
}
|
||||||
|
function LogControls({ getLogs }: LogControlsProps): JSX.Element | null {
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
logLinesPerPage,
|
||||||
|
idStart,
|
||||||
|
idEnd,
|
||||||
|
liveTail,
|
||||||
|
searchFilter: { queryString },
|
||||||
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleLogLinesPerPageChange = (e: number): void => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOG_LINES_PER_PAGE,
|
||||||
|
payload: e,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoToLatest = (): void => {
|
||||||
|
dispatch({
|
||||||
|
type: RESET_ID_START_AND_END,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (liveTail === 'STOPPED')
|
||||||
|
getLogs({
|
||||||
|
q: queryString,
|
||||||
|
limit: logLinesPerPage,
|
||||||
|
orderBy: 'timestamp',
|
||||||
|
order: 'desc',
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
...(idStart ? { idGt: idStart } : {}),
|
||||||
|
...(idEnd ? { idLt: idEnd } : {}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigatePrevious = (): void => {
|
||||||
|
dispatch({
|
||||||
|
type: GET_PREVIOUS_LOG_LINES,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleNavigateNext = (): void => {
|
||||||
|
dispatch({
|
||||||
|
type: GET_NEXT_LOG_LINES,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (liveTail !== 'STOPPED') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Button size="small" type="link" onClick={handleGoToLatest}>
|
||||||
|
<FastBackwardOutlined /> Go to latest
|
||||||
|
</Button>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<Button size="small" type="link" onClick={handleNavigatePrevious}>
|
||||||
|
<LeftOutlined /> Previous
|
||||||
|
</Button>
|
||||||
|
<Button size="small" type="link" onClick={handleNavigateNext}>
|
||||||
|
Next <RightOutlined />
|
||||||
|
</Button>
|
||||||
|
<Select
|
||||||
|
style={{ width: 120 }}
|
||||||
|
value={logLinesPerPage}
|
||||||
|
onChange={handleLogLinesPerPageChange}
|
||||||
|
>
|
||||||
|
{ITEMS_PER_PAGE_OPTIONS.map((count) => {
|
||||||
|
return <Option key={count} value={count}>{`${count} / page`}</Option>;
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
getLogs: (
|
||||||
|
props: Parameters<typeof getLogs>[0],
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
getLogs: bindActionCreators(getLogs, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(memo(LogControls));
|
9
frontend/src/container/LogControls/styles.ts
Normal file
9
frontend/src/container/LogControls/styles.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
`;
|
167
frontend/src/container/LogDetailedView/ActionItem.tsx
Normal file
167
frontend/src/container/LogDetailedView/ActionItem.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Col, Popover } from 'antd';
|
||||||
|
import getStep from 'lib/getStep';
|
||||||
|
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||||
|
import React, { memo, useMemo } from 'react';
|
||||||
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { getLogs } from 'store/actions/logs/getLogs';
|
||||||
|
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { SET_SEARCH_QUERY_STRING, TOGGLE_LIVE_TAIL } from 'types/actions/logs';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
const removeJSONStringifyQuotes = (s: string): string => {
|
||||||
|
if (!s || !s.length) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s[0] === '"' && s[s.length - 1] === '"') {
|
||||||
|
return s.slice(1, s.length - 1);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ActionItemProps {
|
||||||
|
fieldKey: string;
|
||||||
|
fieldValue: string;
|
||||||
|
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||||
|
getLogsAggregate: (
|
||||||
|
props: Parameters<typeof getLogsAggregate>[0],
|
||||||
|
) => ReturnType<typeof getLogsAggregate>;
|
||||||
|
}
|
||||||
|
function ActionItem({
|
||||||
|
fieldKey,
|
||||||
|
fieldValue,
|
||||||
|
getLogs,
|
||||||
|
getLogsAggregate,
|
||||||
|
}: ActionItemProps): JSX.Element | unknown {
|
||||||
|
const {
|
||||||
|
searchFilter: { queryString },
|
||||||
|
logLinesPerPage,
|
||||||
|
idStart,
|
||||||
|
liveTail,
|
||||||
|
idEnd,
|
||||||
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQueryAdd = (newQueryString: string): void => {
|
||||||
|
let updatedQueryString = queryString || '';
|
||||||
|
|
||||||
|
if (updatedQueryString.length === 0) {
|
||||||
|
updatedQueryString += `${newQueryString}`;
|
||||||
|
} else {
|
||||||
|
updatedQueryString += ` AND ${newQueryString}`;
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: SET_SEARCH_QUERY_STRING,
|
||||||
|
payload: updatedQueryString,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (liveTail === 'STOPPED') {
|
||||||
|
getLogs({
|
||||||
|
q: updatedQueryString,
|
||||||
|
limit: logLinesPerPage,
|
||||||
|
orderBy: 'timestamp',
|
||||||
|
order: 'desc',
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
...(idStart ? { idGt: idStart } : {}),
|
||||||
|
...(idEnd ? { idLt: idEnd } : {}),
|
||||||
|
});
|
||||||
|
getLogsAggregate({
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
step: getStep({
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
}),
|
||||||
|
q: updatedQueryString,
|
||||||
|
});
|
||||||
|
} else if (liveTail === 'PLAYING') {
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: 'PAUSED',
|
||||||
|
});
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: liveTail,
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const validatedFieldValue = removeJSONStringifyQuotes(fieldValue);
|
||||||
|
const PopOverMenuContent = useMemo(
|
||||||
|
() => (
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={(): void =>
|
||||||
|
handleQueryAdd(
|
||||||
|
generateFilterQuery({
|
||||||
|
fieldKey,
|
||||||
|
fieldValue: validatedFieldValue,
|
||||||
|
type: 'IN',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PlusCircleOutlined /> Filter for value
|
||||||
|
</Button>
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={(): void =>
|
||||||
|
handleQueryAdd(
|
||||||
|
generateFilterQuery({
|
||||||
|
fieldKey,
|
||||||
|
fieldValue: validatedFieldValue,
|
||||||
|
type: 'NIN',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MinusCircleOutlined /> Filter out value
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[fieldKey, validatedFieldValue],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Popover placement="bottomLeft" content={PopOverMenuContent} trigger="click">
|
||||||
|
<Button type="text" size="small">
|
||||||
|
...
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
interface DispatchProps {
|
||||||
|
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
|
||||||
|
getLogsAggregate: (
|
||||||
|
props: Parameters<typeof getLogsAggregate>[0],
|
||||||
|
) => (dispatch: never) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
getLogs: bindActionCreators(getLogs, dispatch),
|
||||||
|
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export default connect(null, mapDispatchToProps)(memo(ActionItem as any));
|
44
frontend/src/container/LogDetailedView/JsonView.tsx
Normal file
44
frontend/src/container/LogDetailedView/JsonView.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { blue } from '@ant-design/colors';
|
||||||
|
import { CopyFilled } from '@ant-design/icons';
|
||||||
|
import { Button, Row } from 'antd';
|
||||||
|
import Editor from 'components/Editor';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
interface JSONViewProps {
|
||||||
|
logData: ILog;
|
||||||
|
}
|
||||||
|
function JSONView({ logData }: JSONViewProps): JSX.Element {
|
||||||
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
|
const LogJsonData = useMemo(() => JSON.stringify(logData, null, 2), [logData]);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row
|
||||||
|
style={{
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
margin: '0.5rem 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
onClick={(): void => copyToClipboard(LogJsonData)}
|
||||||
|
>
|
||||||
|
<CopyFilled /> <span style={{ color: blue[5] }}>Copy to Clipboard</span>
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
<div style={{ marginTop: '0.5rem' }}>
|
||||||
|
<Editor
|
||||||
|
value={LogJsonData}
|
||||||
|
language="json"
|
||||||
|
height="70vh"
|
||||||
|
readOnly
|
||||||
|
onChange={(): void => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JSONView;
|
109
frontend/src/container/LogDetailedView/TableView.tsx
Normal file
109
frontend/src/container/LogDetailedView/TableView.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { blue, orange } from '@ant-design/colors';
|
||||||
|
import { Input, Table } from 'antd';
|
||||||
|
import AddToQueryHOC from 'components/Logs/AddToQueryHOC';
|
||||||
|
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||||
|
import flatten from 'flat';
|
||||||
|
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
import ActionItem from './ActionItem';
|
||||||
|
|
||||||
|
// Fields which should be restricted from adding it to query
|
||||||
|
const RESTRICTED_FIELDS = ['timestamp'];
|
||||||
|
|
||||||
|
interface TableViewProps {
|
||||||
|
logData: ILog;
|
||||||
|
}
|
||||||
|
function TableView({ logData }: TableViewProps): JSX.Element | null {
|
||||||
|
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
||||||
|
|
||||||
|
const flattenLogData: Record<string, never> | null = useMemo(
|
||||||
|
() => (logData ? flatten(logData) : null),
|
||||||
|
[logData],
|
||||||
|
);
|
||||||
|
if (logData === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSource =
|
||||||
|
flattenLogData !== null &&
|
||||||
|
Object.keys(flattenLogData)
|
||||||
|
.filter((field) => fieldSearchFilter(field, fieldSearchInput))
|
||||||
|
.map((key) => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
field: key,
|
||||||
|
value: JSON.stringify(flattenLogData[key]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dataSource) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
width: 75,
|
||||||
|
render: (fieldData: Record<string, string>): JSX.Element | null => {
|
||||||
|
const fieldKey = fieldData.field.split('.').slice(-1);
|
||||||
|
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
||||||
|
return <ActionItem fieldKey={fieldKey} fieldValue={fieldData.value} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Field',
|
||||||
|
dataIndex: 'field',
|
||||||
|
key: 'field',
|
||||||
|
width: '35%',
|
||||||
|
render: (field: string): JSX.Element => {
|
||||||
|
const fieldKey = field.split('.').slice(-1);
|
||||||
|
const renderedField = <span style={{ color: blue[4] }}>{field}</span>;
|
||||||
|
|
||||||
|
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
||||||
|
return (
|
||||||
|
<AddToQueryHOC fieldKey={fieldKey[0]} fieldValue={flattenLogData[field]}>
|
||||||
|
{' '}
|
||||||
|
{renderedField}
|
||||||
|
</AddToQueryHOC>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return renderedField;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Value',
|
||||||
|
dataIndex: 'value',
|
||||||
|
key: 'value',
|
||||||
|
ellipsis: false,
|
||||||
|
render: (field: never): JSX.Element => (
|
||||||
|
<CopyClipboardHOC textToCopy={field}>
|
||||||
|
<span style={{ color: orange[6] }}>{field}</span>
|
||||||
|
</CopyClipboardHOC>
|
||||||
|
),
|
||||||
|
width: '60%',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<Input
|
||||||
|
placeholder="Search field names"
|
||||||
|
size="large"
|
||||||
|
value={fieldSearchInput}
|
||||||
|
onChange={(e): void => setFieldSearchInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
// scroll={{ x: true }}
|
||||||
|
tableLayout="fixed"
|
||||||
|
dataSource={dataSource}
|
||||||
|
columns={columns as never}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableView;
|
53
frontend/src/container/LogDetailedView/index.tsx
Normal file
53
frontend/src/container/LogDetailedView/index.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Drawer, Tabs } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import JSONView from './JsonView';
|
||||||
|
import TableView from './TableView';
|
||||||
|
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
function LogDetailedView(): JSX.Element {
|
||||||
|
const { detailedLog } = useSelector<AppState, ILogsReducer>(
|
||||||
|
(state) => state.logs,
|
||||||
|
);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const onDrawerClose = (): void => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_DETAILED_LOG_DATA,
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{}}>
|
||||||
|
<Drawer
|
||||||
|
width="60%"
|
||||||
|
title="Log Details"
|
||||||
|
placement="right"
|
||||||
|
closable
|
||||||
|
mask={false}
|
||||||
|
onClose={onDrawerClose}
|
||||||
|
visible={detailedLog !== null}
|
||||||
|
getContainer={false}
|
||||||
|
style={{ overscrollBehavior: 'contain' }}
|
||||||
|
>
|
||||||
|
{detailedLog && (
|
||||||
|
<Tabs defaultActiveKey="1">
|
||||||
|
<TabPane tab="Table" key="1">
|
||||||
|
<TableView logData={detailedLog} />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="JSON" key="2">
|
||||||
|
<JSONView logData={detailedLog} />
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogDetailedView;
|
26
frontend/src/container/LogLiveTail/OptionIcon.tsx
Normal file
26
frontend/src/container/LogLiveTail/OptionIcon.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface OptionIconProps {
|
||||||
|
isDarkMode: boolean;
|
||||||
|
}
|
||||||
|
function OptionIcon({ isDarkMode }: OptionIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="1rem"
|
||||||
|
height="1rem"
|
||||||
|
viewBox="0 0 52 52"
|
||||||
|
enableBackground="new 0 0 52 52"
|
||||||
|
fill={isDarkMode ? '#eee' : '#222'}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20,44c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6S20,47.3,20,44z M20,26c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6
|
||||||
|
S20,29.3,20,26z M20,8c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6S20,11.3,20,8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OptionIcon;
|
222
frontend/src/container/LogLiveTail/index.tsx
Normal file
222
frontend/src/container/LogLiveTail/index.tsx
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { green } from '@ant-design/colors';
|
||||||
|
import { PauseOutlined, PlayCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Popover, Row, Select } from 'antd';
|
||||||
|
import { LiveTail } from 'api/logs/livetail';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { throttle } from 'lodash-es';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import {
|
||||||
|
FLUSH_LOGS,
|
||||||
|
PUSH_LIVE_TAIL_EVENT,
|
||||||
|
SET_LIVE_TAIL_START_TIME,
|
||||||
|
TOGGLE_LIVE_TAIL,
|
||||||
|
} from 'types/actions/logs';
|
||||||
|
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import OptionIcon from './OptionIcon';
|
||||||
|
import { TimePickerCard, TimePickerSelect } from './styles';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const TIME_PICKER_OPTIONS = [
|
||||||
|
{
|
||||||
|
value: 5,
|
||||||
|
label: '5m',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 15,
|
||||||
|
label: '15m',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 30,
|
||||||
|
label: '30m',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 60,
|
||||||
|
label: '1hr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 360,
|
||||||
|
label: '6hrs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 720,
|
||||||
|
label: '12hrs',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function LogLiveTail(): JSX.Element {
|
||||||
|
const {
|
||||||
|
liveTail,
|
||||||
|
searchFilter: { queryString },
|
||||||
|
liveTailStartRange,
|
||||||
|
logs,
|
||||||
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const handleLiveTail = (toggleState: TLogsLiveTailState): void => {
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: toggleState,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const batchedEventsRef = useRef<Record<string, unknown>[]>([]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const pushLiveLog = useCallback(
|
||||||
|
throttle(() => {
|
||||||
|
dispatch({
|
||||||
|
type: PUSH_LIVE_TAIL_EVENT,
|
||||||
|
payload: batchedEventsRef.current.reverse(),
|
||||||
|
});
|
||||||
|
// console.log('DISPATCH', batchedEventsRef.current.length);
|
||||||
|
batchedEventsRef.current = [];
|
||||||
|
}, 1500),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const batchLiveLog = (e: { data: string }): void => {
|
||||||
|
// console.log('EVENT BATCHED');
|
||||||
|
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
|
||||||
|
pushLiveLog();
|
||||||
|
};
|
||||||
|
|
||||||
|
// This ref depicts thats whether the live tail is played from paused state or not.
|
||||||
|
const liveTailSourceRef = useRef<EventSource | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (liveTail === 'PLAYING') {
|
||||||
|
// console.log('Starting Live Tail', logs.length);
|
||||||
|
const timeStamp = dayjs().subtract(liveTailStartRange, 'minute').valueOf();
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
...(queryString ? { q: queryString } : {}),
|
||||||
|
timestampStart: (timeStamp * 1e6) as never,
|
||||||
|
...(liveTailSourceRef.current && logs.length > 0
|
||||||
|
? {
|
||||||
|
idGt: logs[0].id,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
const source = LiveTail(queryParams.toString());
|
||||||
|
liveTailSourceRef.current = source;
|
||||||
|
source.onmessage = function connectionMessage(e): void {
|
||||||
|
batchLiveLog(e);
|
||||||
|
};
|
||||||
|
// source.onopen = function connectionOpen(): void { };
|
||||||
|
source.onerror = function connectionError(event: unknown): void {
|
||||||
|
console.error(event);
|
||||||
|
source.close();
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else if (liveTailSourceRef.current && liveTailSourceRef.current.close) {
|
||||||
|
liveTailSourceRef.current?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (liveTail === 'STOPPED') {
|
||||||
|
liveTailSourceRef.current = null;
|
||||||
|
}
|
||||||
|
}, [liveTail]);
|
||||||
|
|
||||||
|
const handleLiveTailStart = (): void => {
|
||||||
|
handleLiveTail('PLAYING');
|
||||||
|
if (!liveTailSourceRef.current) {
|
||||||
|
dispatch({
|
||||||
|
type: FLUSH_LOGS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const OptionsPopOverContent = useMemo(
|
||||||
|
() => (
|
||||||
|
<TimePickerSelect
|
||||||
|
disabled={liveTail === 'PLAYING'}
|
||||||
|
value={liveTailStartRange}
|
||||||
|
onChange={(value): void => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_LIVE_TAIL_START_TIME,
|
||||||
|
payload: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{TIME_PICKER_OPTIONS.map((optionData) => (
|
||||||
|
<Option key={optionData.label} value={optionData.value}>
|
||||||
|
Last {optionData.label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</TimePickerSelect>
|
||||||
|
),
|
||||||
|
[dispatch, liveTail, liveTailStartRange],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<TimePickerCard>
|
||||||
|
<Row
|
||||||
|
style={{ gap: '0.5rem', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{liveTail === 'PLAYING' ? (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={(): void => handleLiveTail('PAUSED')}
|
||||||
|
title="Pause live tail"
|
||||||
|
style={{ background: green[6] }}
|
||||||
|
>
|
||||||
|
Pause <PauseOutlined />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleLiveTailStart}
|
||||||
|
title="Start live tail"
|
||||||
|
>
|
||||||
|
Go Live <PlayCircleOutlined />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{liveTail !== 'STOPPED' && (
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={(): void => handleLiveTail('STOPPED')}
|
||||||
|
title="Exit live tail"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '0.8rem',
|
||||||
|
width: '0.8rem',
|
||||||
|
background: isDarkMode ? '#eee' : '#222',
|
||||||
|
borderRadius: '0.1rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
placement="bottomRight"
|
||||||
|
title="Select Live Tail Timing"
|
||||||
|
trigger="click"
|
||||||
|
content={OptionsPopOverContent}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
padding: '0.3rem 0.4rem 0.3rem 0',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OptionIcon isDarkMode={isDarkMode} />
|
||||||
|
</span>
|
||||||
|
</Popover>
|
||||||
|
</Row>
|
||||||
|
</TimePickerCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogLiveTail;
|
12
frontend/src/container/LogLiveTail/styles.ts
Normal file
12
frontend/src/container/LogLiveTail/styles.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Card, Select } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const TimePickerCard = styled(Card)`
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TimePickerSelect = styled(Select)`
|
||||||
|
min-width: 100px;
|
||||||
|
`;
|
71
frontend/src/container/Logs/index.tsx
Normal file
71
frontend/src/container/Logs/index.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { Divider, Row } from 'antd';
|
||||||
|
import LogControls from 'container/LogControls';
|
||||||
|
import LogDetailedView from 'container/LogDetailedView';
|
||||||
|
import LogLiveTail from 'container/LogLiveTail';
|
||||||
|
import LogsAggregate from 'container/LogsAggregate';
|
||||||
|
import LogsFilters from 'container/LogsFilters';
|
||||||
|
import SearchFilter from 'container/LogsSearchFilter';
|
||||||
|
import LogsTable from 'container/LogsTable';
|
||||||
|
import React, { memo, useEffect, useMemo } from 'react';
|
||||||
|
import { connect, useDispatch } from 'react-redux';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { GetLogsFields } from 'store/actions/logs/getFields';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
|
||||||
|
|
||||||
|
interface LogsProps {
|
||||||
|
getLogsFields: VoidFunction;
|
||||||
|
}
|
||||||
|
function Logs({ getLogsFields }: LogsProps): JSX.Element {
|
||||||
|
const { search } = useLocation();
|
||||||
|
|
||||||
|
const urlQuery = useMemo(() => {
|
||||||
|
return new URLSearchParams(search);
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_SEARCH_QUERY_STRING,
|
||||||
|
payload: urlQuery.get('q'),
|
||||||
|
});
|
||||||
|
}, [dispatch, urlQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLogsFields();
|
||||||
|
}, [getLogsFields]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<Row style={{ justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<SearchFilter />
|
||||||
|
<Divider type="vertical" style={{ height: '2rem' }} />
|
||||||
|
<LogLiveTail />
|
||||||
|
</Row>
|
||||||
|
<LogsAggregate />
|
||||||
|
<LogControls />
|
||||||
|
<Divider style={{ margin: 0 }} />
|
||||||
|
<Row gutter={20} style={{ flexWrap: 'nowrap' }}>
|
||||||
|
<LogsFilters />
|
||||||
|
<Divider type="vertical" style={{ height: '100%', margin: 0 }} />
|
||||||
|
<LogsTable />
|
||||||
|
</Row>
|
||||||
|
<LogDetailedView />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(memo(Logs));
|
133
frontend/src/container/LogsAggregate/index.tsx
Normal file
133
frontend/src/container/LogsAggregate/index.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { blue } from '@ant-design/colors';
|
||||||
|
import Graph from 'components/Graph';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import getStep from 'lib/getStep';
|
||||||
|
import React, { memo, useEffect, useRef } from 'react';
|
||||||
|
import { connect, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import { Container } from './styles';
|
||||||
|
|
||||||
|
interface LogsAggregateProps {
|
||||||
|
getLogsAggregate: (arg0: Parameters<typeof getLogsAggregate>[0]) => void;
|
||||||
|
}
|
||||||
|
function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
searchFilter: { queryString },
|
||||||
|
idEnd,
|
||||||
|
idStart,
|
||||||
|
isLoadingAggregate,
|
||||||
|
logsAggregate,
|
||||||
|
liveTail,
|
||||||
|
liveTailStartRange,
|
||||||
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
|
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const reFetchIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
switch (liveTail) {
|
||||||
|
case 'STOPPED': {
|
||||||
|
if (reFetchIntervalRef.current) {
|
||||||
|
clearInterval(reFetchIntervalRef.current);
|
||||||
|
}
|
||||||
|
reFetchIntervalRef.current = null;
|
||||||
|
getLogsAggregate({
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
step: getStep({
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
}),
|
||||||
|
q: queryString,
|
||||||
|
...(idStart ? { idGt: idStart } : {}),
|
||||||
|
...(idEnd ? { idLt: idEnd } : {}),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'PLAYING': {
|
||||||
|
const aggregateCall = (): void => {
|
||||||
|
const startTime =
|
||||||
|
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
|
||||||
|
const endTime = dayjs().valueOf() * 1e6;
|
||||||
|
getLogsAggregate({
|
||||||
|
timestampStart: startTime,
|
||||||
|
timestampEnd: endTime,
|
||||||
|
step: getStep({
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
}),
|
||||||
|
q: queryString,
|
||||||
|
...(idStart ? { idGt: idStart } : {}),
|
||||||
|
...(idEnd ? { idLt: idEnd } : {}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
aggregateCall();
|
||||||
|
reFetchIntervalRef.current = setInterval(aggregateCall, 60000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'PAUSED': {
|
||||||
|
if (reFetchIntervalRef.current) {
|
||||||
|
clearInterval(reFetchIntervalRef.current);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [getLogsAggregate, maxTime, minTime, liveTail]);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: logsAggregate.map((s) => s.value),
|
||||||
|
backgroundColor: blue[4],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{isLoadingAggregate ? (
|
||||||
|
<Spinner size="default" height="100%" />
|
||||||
|
) : (
|
||||||
|
<Graph
|
||||||
|
name="usage"
|
||||||
|
data={data}
|
||||||
|
type="bar"
|
||||||
|
containerHeight="100%"
|
||||||
|
animate={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
getLogsAggregate: (
|
||||||
|
props: Parameters<typeof getLogsAggregate>[0],
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(memo(LogsAggregate));
|
11
frontend/src/container/LogsAggregate/styles.ts
Normal file
11
frontend/src/container/LogsAggregate/styles.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Card } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Container = styled(Card)`
|
||||||
|
position: relative;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
.ant-card-body {
|
||||||
|
height: 20vh;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
`;
|
57
frontend/src/container/LogsFilters/FieldItem.tsx
Normal file
57
frontend/src/container/LogsFilters/FieldItem.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Popover, Spin } from 'antd';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import { Field } from './styles';
|
||||||
|
|
||||||
|
interface FieldItemProps {
|
||||||
|
name: string;
|
||||||
|
buttonIcon: React.ReactNode;
|
||||||
|
buttonOnClick: (arg0: Record<string, unknown>) => void;
|
||||||
|
fieldData: Record<string, never>;
|
||||||
|
fieldIndex: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
iconHoverText: string;
|
||||||
|
}
|
||||||
|
export function FieldItem({
|
||||||
|
name,
|
||||||
|
buttonIcon,
|
||||||
|
buttonOnClick,
|
||||||
|
fieldData,
|
||||||
|
fieldIndex,
|
||||||
|
isLoading,
|
||||||
|
iconHoverText,
|
||||||
|
}: FieldItemProps): JSX.Element {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
onMouseEnter={(): void => {
|
||||||
|
setIsHovered(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={(): void => setIsHovered(false)}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
>
|
||||||
|
<span>{name}</span>
|
||||||
|
{isLoading ? (
|
||||||
|
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />
|
||||||
|
) : (
|
||||||
|
isHovered &&
|
||||||
|
buttonOnClick && (
|
||||||
|
<Popover content={<span>{iconHoverText}</span>}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={buttonIcon}
|
||||||
|
onClick={(): void => buttonOnClick({ fieldData, fieldIndex })}
|
||||||
|
style={{ color: 'inherit', padding: 0, height: '1rem', width: '1rem' }}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
153
frontend/src/container/LogsFilters/index.tsx
Normal file
153
frontend/src/container/LogsFilters/index.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/* eslint-disable react/no-array-index-key */
|
||||||
|
import { red } from '@ant-design/colors';
|
||||||
|
import { CloseOutlined, PlusCircleFilled } from '@ant-design/icons';
|
||||||
|
import { Input } from 'antd';
|
||||||
|
import AddToSelectedFields from 'api/logs/AddToSelectedField';
|
||||||
|
import RemoveSelectedField from 'api/logs/RemoveFromSelectedField';
|
||||||
|
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||||
|
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||||
|
import React, { memo, useState } from 'react';
|
||||||
|
import { connect, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { GetLogsFields } from 'store/actions/logs/getFields';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { IInterestingFields, ISelectedFields } from 'types/api/logs/fields';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import { FieldItem } from './FieldItem';
|
||||||
|
import { CategoryContainer, Container, FieldContainer } from './styles';
|
||||||
|
|
||||||
|
const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
|
||||||
|
|
||||||
|
interface LogsFiltersProps {
|
||||||
|
getLogsFields: () => void;
|
||||||
|
}
|
||||||
|
function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
fields: { interesting, selected },
|
||||||
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
|
|
||||||
|
const [selectedFieldLoading, setSelectedFieldLoading] = useState<number[]>([]);
|
||||||
|
const [interestingFieldLoading, setInterestingFieldLoading] = useState<
|
||||||
|
number[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const [filterValuesInput, setFilterValuesInput] = useState('');
|
||||||
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setFilterValuesInput((e.target as HTMLInputElement).value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddInterestingToSelected = async ({
|
||||||
|
fieldData,
|
||||||
|
fieldIndex,
|
||||||
|
}: {
|
||||||
|
fieldData: IInterestingFields;
|
||||||
|
fieldIndex: number;
|
||||||
|
}): Promise<void> => {
|
||||||
|
setInterestingFieldLoading((prevState: number[]) => {
|
||||||
|
prevState.push(fieldIndex);
|
||||||
|
return [...prevState];
|
||||||
|
});
|
||||||
|
|
||||||
|
await AddToSelectedFields({
|
||||||
|
...fieldData,
|
||||||
|
selected: true,
|
||||||
|
});
|
||||||
|
getLogsFields();
|
||||||
|
|
||||||
|
setInterestingFieldLoading(
|
||||||
|
interestingFieldLoading.filter((e) => e !== fieldIndex),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const handleRemoveSelectedField = async ({
|
||||||
|
fieldData,
|
||||||
|
fieldIndex,
|
||||||
|
}: {
|
||||||
|
fieldData: ISelectedFields;
|
||||||
|
fieldIndex: number;
|
||||||
|
}): Promise<void> => {
|
||||||
|
setSelectedFieldLoading((prevState) => {
|
||||||
|
prevState.push(fieldIndex);
|
||||||
|
return [...prevState];
|
||||||
|
});
|
||||||
|
|
||||||
|
await RemoveSelectedField({
|
||||||
|
...fieldData,
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
getLogsFields();
|
||||||
|
|
||||||
|
setSelectedFieldLoading(
|
||||||
|
interestingFieldLoading.filter((e) => e !== fieldIndex),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Container flex="450px">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter Values"
|
||||||
|
onInput={handleSearch}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={filterValuesInput}
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CategoryContainer>
|
||||||
|
<CategoryHeading>SELECTED FIELDS</CategoryHeading>
|
||||||
|
<FieldContainer>
|
||||||
|
{selected
|
||||||
|
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
|
||||||
|
.map((field, idx) => (
|
||||||
|
<FieldItem
|
||||||
|
key={`${JSON.stringify(field)}-${idx}`}
|
||||||
|
name={field.name}
|
||||||
|
fieldData={field as never}
|
||||||
|
fieldIndex={idx}
|
||||||
|
buttonIcon={<CloseOutlined style={{ color: red[5] }} />}
|
||||||
|
buttonOnClick={
|
||||||
|
(!RESTRICTED_SELECTED_FIELDS.includes(field.name) &&
|
||||||
|
handleRemoveSelectedField) as never
|
||||||
|
}
|
||||||
|
isLoading={selectedFieldLoading.includes(idx)}
|
||||||
|
iconHoverText="Remove from Selected Fields"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FieldContainer>
|
||||||
|
</CategoryContainer>
|
||||||
|
<CategoryContainer>
|
||||||
|
<CategoryHeading>INTERESTING FIELDS</CategoryHeading>
|
||||||
|
<FieldContainer>
|
||||||
|
{interesting
|
||||||
|
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
|
||||||
|
.map((field, idx) => (
|
||||||
|
<FieldItem
|
||||||
|
key={`${JSON.stringify(field)}-${idx}`}
|
||||||
|
name={field.name}
|
||||||
|
fieldData={field as never}
|
||||||
|
fieldIndex={idx}
|
||||||
|
buttonIcon={<PlusCircleFilled />}
|
||||||
|
buttonOnClick={handleAddInterestingToSelected as never}
|
||||||
|
isLoading={interestingFieldLoading.includes(idx)}
|
||||||
|
iconHoverText="Add to Selected Fields"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FieldContainer>
|
||||||
|
</CategoryContainer>
|
||||||
|
{/* <ExtractField>Extract Fields</ExtractField> */}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(memo(LogsFilters));
|
36
frontend/src/container/LogsFilters/styles.ts
Normal file
36
frontend/src/container/LogsFilters/styles.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { blue, grey } from '@ant-design/colors';
|
||||||
|
import { Col, Typography } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Container = styled(Col)`
|
||||||
|
padding-top: 0.3rem;
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 350px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CategoryContainer = styled.div`
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FieldContainer = styled(Typography.Text)`
|
||||||
|
margin: 0.2rem 0;
|
||||||
|
color: ${blue[4]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Field = styled.div<{ isDarkMode: boolean }>`
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
&:hover {
|
||||||
|
background: ${({ isDarkMode }): string => {
|
||||||
|
return isDarkMode ? grey[7] : '#ddd';
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ExtractField = styled(Typography.Text)`
|
||||||
|
color: ${blue[4]};
|
||||||
|
`;
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface FieldKeyProps {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldKey({ name, type }: FieldKeyProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<span style={{ margin: '0.25rem 0', display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<Typography.Text>{name}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" italic>
|
||||||
|
{type}
|
||||||
|
</Typography.Text>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FieldKey;
|
@ -0,0 +1,263 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable react/no-array-index-key */
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Input, Select } from 'antd';
|
||||||
|
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||||
|
import {
|
||||||
|
ConditionalOperators,
|
||||||
|
QueryOperatorsMultiVal,
|
||||||
|
QueryOperatorsSingleVal,
|
||||||
|
} from 'lib/logql/tokens';
|
||||||
|
import { flatten } from 'lodash-es';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import FieldKey from '../FieldKey';
|
||||||
|
import { QueryConditionContainer, QueryFieldContainer } from '../styles';
|
||||||
|
import { createParsedQueryStructure } from '../utils';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
interface QueryFieldProps {
|
||||||
|
query: { value: string | string[]; type: string }[];
|
||||||
|
queryIndex: number;
|
||||||
|
onUpdate: (query: unknown, queryIndex: number) => void;
|
||||||
|
onDelete: (queryIndex: number) => void;
|
||||||
|
}
|
||||||
|
function QueryField({
|
||||||
|
query,
|
||||||
|
queryIndex,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
}: QueryFieldProps): JSX.Element | null {
|
||||||
|
const {
|
||||||
|
fields: { selected },
|
||||||
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
|
|
||||||
|
const getFieldType = (inputKey: string): string => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const selectedField of selected) {
|
||||||
|
if (inputKey === selectedField.name) {
|
||||||
|
return selectedField.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
const fieldType = useMemo(() => getFieldType(query[0].value as string), [
|
||||||
|
query,
|
||||||
|
]);
|
||||||
|
const handleChange = (qIdx: number, value: string): void => {
|
||||||
|
query[qIdx].value = value || '';
|
||||||
|
|
||||||
|
if (qIdx === 1) {
|
||||||
|
if (Object.values(QueryOperatorsMultiVal).includes(value)) {
|
||||||
|
if (!Array.isArray(query[2].value)) {
|
||||||
|
query[2].value = [];
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
Object.values(QueryOperatorsSingleVal).includes(value) &&
|
||||||
|
Array.isArray(query[2].value)
|
||||||
|
) {
|
||||||
|
query[2].value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onUpdate(query, queryIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = (): void => {
|
||||||
|
onDelete(queryIndex);
|
||||||
|
};
|
||||||
|
if (!Array.isArray(query)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryFieldContainer
|
||||||
|
style={{ ...(queryIndex === 0 && { gridColumnStart: 2 }) }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, minWidth: 100 }}>
|
||||||
|
<FieldKey name={(query[0] && query[0].value) as string} type={fieldType} />
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
defaultActiveFirstOption={false}
|
||||||
|
placeholder="Select Operator"
|
||||||
|
defaultValue={
|
||||||
|
query[1] && query[1].value
|
||||||
|
? (query[1].value as string).toUpperCase()
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onChange={(e): void => handleChange(1, e)}
|
||||||
|
style={{ minWidth: 150 }}
|
||||||
|
>
|
||||||
|
{Object.values({
|
||||||
|
...QueryOperatorsMultiVal,
|
||||||
|
...QueryOperatorsSingleVal,
|
||||||
|
}).map((cond) => (
|
||||||
|
<Option key={cond} value={cond} label={cond}>
|
||||||
|
{cond}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<div style={{ flex: 2 }}>
|
||||||
|
{Array.isArray(query[2].value) ||
|
||||||
|
Object.values(QueryOperatorsMultiVal).some(
|
||||||
|
(op) => op.toUpperCase() === (query[1].value as string)?.toUpperCase(),
|
||||||
|
) ? (
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onChange={(e): void => handleChange(2, e as never)}
|
||||||
|
defaultValue={(query[2] && query[2].value) || []}
|
||||||
|
notFoundContent={null}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
onChange={(e): void => handleChange(2, e.target.value)}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
defaultValue={query[2] && query[2].value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={handleClear}
|
||||||
|
/>
|
||||||
|
</QueryFieldContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryConditionFieldProps {
|
||||||
|
query: { value: string | string[]; type: string }[];
|
||||||
|
queryIndex: number;
|
||||||
|
onUpdate: (arg0: unknown, arg1: number) => void;
|
||||||
|
}
|
||||||
|
function QueryConditionField({
|
||||||
|
query,
|
||||||
|
queryIndex,
|
||||||
|
onUpdate,
|
||||||
|
}: QueryConditionFieldProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<QueryConditionContainer>
|
||||||
|
<Select
|
||||||
|
defaultValue={
|
||||||
|
(query as any).value &&
|
||||||
|
(((query as any)?.value as any) as string).toUpperCase()
|
||||||
|
}
|
||||||
|
onChange={(e): void => {
|
||||||
|
onUpdate({ ...query, value: e }, queryIndex);
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{Object.values(ConditionalOperators).map((cond) => (
|
||||||
|
<Option key={cond} value={cond} label={cond}>
|
||||||
|
{cond}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</QueryConditionContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const hashCode = (s: string): string => {
|
||||||
|
if (!s) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return `${Math.abs(
|
||||||
|
s.split('').reduce((a, b) => {
|
||||||
|
a = (a << 5) - a + b.charCodeAt(0);
|
||||||
|
return a & a;
|
||||||
|
}, 0),
|
||||||
|
)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function QueryBuilder({
|
||||||
|
updateParsedQuery,
|
||||||
|
}: {
|
||||||
|
updateParsedQuery: (arg0: unknown) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const {
|
||||||
|
searchFilter: { parsedQuery },
|
||||||
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
|
|
||||||
|
const keyPrefixRef = useRef(hashCode(JSON.stringify(parsedQuery)));
|
||||||
|
const [keyPrefix, setKeyPrefix] = useState(keyPrefixRef.current);
|
||||||
|
const generatedQueryStructure = createParsedQueryStructure(
|
||||||
|
parsedQuery as never[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const incomingHashCode = hashCode(JSON.stringify(parsedQuery));
|
||||||
|
if (incomingHashCode !== keyPrefixRef.current) {
|
||||||
|
keyPrefixRef.current = incomingHashCode;
|
||||||
|
setKeyPrefix(incomingHashCode);
|
||||||
|
}
|
||||||
|
}, [parsedQuery]);
|
||||||
|
|
||||||
|
const handleUpdate = (
|
||||||
|
query: { value: string | string[]; type: string }[],
|
||||||
|
queryIndex: number,
|
||||||
|
): void => {
|
||||||
|
const updatedParsedQuery = generatedQueryStructure;
|
||||||
|
updatedParsedQuery[queryIndex] = query as never;
|
||||||
|
|
||||||
|
const flatParsedQuery = flatten(updatedParsedQuery).filter((q) => q.value);
|
||||||
|
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
|
||||||
|
updateParsedQuery(flatParsedQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (queryIndex: number): void => {
|
||||||
|
const updatedParsedQuery = generatedQueryStructure;
|
||||||
|
updatedParsedQuery.splice(queryIndex - 1, 2);
|
||||||
|
|
||||||
|
const flatParsedQuery = flatten(updatedParsedQuery).filter((q) => q.value);
|
||||||
|
keyPrefixRef.current = v4();
|
||||||
|
updateParsedQuery(flatParsedQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const QueryUI = (): JSX.Element | JSX.Element[] =>
|
||||||
|
generatedQueryStructure.map((query, idx) => {
|
||||||
|
if (Array.isArray(query))
|
||||||
|
return (
|
||||||
|
<QueryField
|
||||||
|
key={keyPrefix + idx}
|
||||||
|
query={query as never}
|
||||||
|
queryIndex={idx}
|
||||||
|
onUpdate={handleUpdate as never}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryConditionField
|
||||||
|
key={keyPrefix + idx}
|
||||||
|
query={query}
|
||||||
|
queryIndex={idx}
|
||||||
|
onUpdate={handleUpdate as never}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '80px 1fr',
|
||||||
|
margin: '0.5rem 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{QueryUI()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueryBuilder;
|
@ -0,0 +1,57 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||||
|
import { map } from 'lodash-es';
|
||||||
|
import React from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { ADD_SEARCH_FIELD_QUERY_STRING } from 'types/actions/logs';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import FieldKey from './FieldKey';
|
||||||
|
|
||||||
|
interface SuggestedItemProps {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
function SuggestedItem({ name, type }: SuggestedItemProps): JSX.Element {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const addSuggestedField = (): void => {
|
||||||
|
dispatch({
|
||||||
|
type: ADD_SEARCH_FIELD_QUERY_STRING,
|
||||||
|
payload: name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{ display: 'block', padding: '0.2rem' }}
|
||||||
|
onClick={addSuggestedField}
|
||||||
|
>
|
||||||
|
<FieldKey name={name} type={type} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Suggestions(): JSX.Element {
|
||||||
|
const {
|
||||||
|
fields: { selected },
|
||||||
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CategoryHeading>SUGGESTIONS</CategoryHeading>
|
||||||
|
<div>
|
||||||
|
{map(selected, (field) => (
|
||||||
|
<SuggestedItem
|
||||||
|
key={JSON.stringify(field)}
|
||||||
|
name={field.name}
|
||||||
|
type={field.type}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Suggestions;
|
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import QueryBuilder from './QueryBuilder/QueryBuilder';
|
||||||
|
import Suggestions from './Suggestions';
|
||||||
|
|
||||||
|
interface SearchFieldsProps {
|
||||||
|
updateParsedQuery: () => void;
|
||||||
|
}
|
||||||
|
function SearchFields({ updateParsedQuery }: SearchFieldsProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<QueryBuilder updateParsedQuery={updateParsedQuery} />
|
||||||
|
<Suggestions />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default SearchFields;
|
@ -0,0 +1,20 @@
|
|||||||
|
import { blue } from '@ant-design/colors';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const QueryFieldContainer = styled.div`
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
margin: 0.1rem 0.5rem 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
gap: 1rem;
|
||||||
|
&:hover {
|
||||||
|
background: ${blue[6]};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const QueryConditionContainer = styled.div`
|
||||||
|
padding: 0.25rem 0rem;
|
||||||
|
margin: 0.1rem 0;
|
||||||
|
`;
|
@ -0,0 +1,61 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { QueryTypes } from 'lib/logql/tokens';
|
||||||
|
|
||||||
|
export const queryKOVPair = () => [
|
||||||
|
{
|
||||||
|
type: QueryTypes.QUERY_KEY,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: QueryTypes.QUERY_OPERATOR,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: QueryTypes.QUERY_VALUE,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const createParsedQueryStructure = (parsedQuery = []) => {
|
||||||
|
if (!parsedQuery.length) {
|
||||||
|
return parsedQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
const structuredArray = [queryKOVPair()];
|
||||||
|
|
||||||
|
let cond;
|
||||||
|
let qCtr = -1;
|
||||||
|
parsedQuery.forEach((query, idx) => {
|
||||||
|
if (cond) {
|
||||||
|
structuredArray.push(cond);
|
||||||
|
structuredArray.push(queryKOVPair());
|
||||||
|
cond = null;
|
||||||
|
qCtr = -1;
|
||||||
|
}
|
||||||
|
const stagingArr = structuredArray[structuredArray.length - 1];
|
||||||
|
const prevQuery =
|
||||||
|
Array.isArray(stagingArr) && qCtr >= 0 ? stagingArr[qCtr] : null;
|
||||||
|
|
||||||
|
if (query.type === QueryTypes.QUERY_KEY) {
|
||||||
|
stagingArr[qCtr + 1] = query;
|
||||||
|
} else if (
|
||||||
|
query.type === QueryTypes.QUERY_OPERATOR &&
|
||||||
|
prevQuery &&
|
||||||
|
prevQuery.type === QueryTypes.QUERY_KEY
|
||||||
|
) {
|
||||||
|
stagingArr[qCtr + 1] = query;
|
||||||
|
} else if (
|
||||||
|
query.type === QueryTypes.QUERY_VALUE &&
|
||||||
|
prevQuery &&
|
||||||
|
prevQuery.type === QueryTypes.QUERY_OPERATOR
|
||||||
|
) {
|
||||||
|
stagingArr[qCtr + 1] = query;
|
||||||
|
} else if (query.type === QueryTypes.CONDITIONAL_OPERATOR) {
|
||||||
|
cond = query;
|
||||||
|
}
|
||||||
|
qCtr++;
|
||||||
|
});
|
||||||
|
return structuredArray;
|
||||||
|
};
|
178
frontend/src/container/LogsSearchFilter/index.tsx
Normal file
178
frontend/src/container/LogsSearchFilter/index.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { CloseSquareOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Input } from 'antd';
|
||||||
|
import useClickOutside from 'hooks/useClickOutside';
|
||||||
|
import getStep from 'lib/getStep';
|
||||||
|
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useLocation } from 'react-use';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { getLogs } from 'store/actions/logs/getLogs';
|
||||||
|
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { TOGGLE_LIVE_TAIL } from 'types/actions/logs';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import SearchFields from './SearchFields';
|
||||||
|
import { DropDownContainer } from './styles';
|
||||||
|
import { useSearchParser } from './useSearchParser';
|
||||||
|
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
interface SearchFilterProps {
|
||||||
|
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||||
|
getLogsAggregate: (
|
||||||
|
props: Parameters<typeof getLogsAggregate>[0],
|
||||||
|
) => ReturnType<typeof getLogsAggregate>;
|
||||||
|
}
|
||||||
|
function SearchFilter({
|
||||||
|
getLogs,
|
||||||
|
getLogsAggregate,
|
||||||
|
}: SearchFilterProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
queryString,
|
||||||
|
updateParsedQuery,
|
||||||
|
updateQueryString,
|
||||||
|
} = useSearchParser();
|
||||||
|
const [showDropDown, setShowDropDown] = useState(false);
|
||||||
|
|
||||||
|
const { logLinesPerPage, idEnd, idStart, liveTail } = useSelector<
|
||||||
|
AppState,
|
||||||
|
ILogsReducer
|
||||||
|
>((state) => state.logs);
|
||||||
|
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchComponentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useClickOutside(searchComponentRef, (e: HTMLElement) => {
|
||||||
|
// using this hack as overlay span is voilating this condition
|
||||||
|
if (
|
||||||
|
e.nodeName === 'svg' ||
|
||||||
|
e.nodeName === 'path' ||
|
||||||
|
e.nodeName === 'span' ||
|
||||||
|
e.nodeName === 'button'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
e.nodeName === 'DIV' &&
|
||||||
|
![
|
||||||
|
'ant-empty-image',
|
||||||
|
'ant-select-item',
|
||||||
|
'ant-col',
|
||||||
|
'ant-select-item-option-content',
|
||||||
|
'ant-select-item-option-active',
|
||||||
|
].find((p) => p.indexOf(e.className) !== -1) &&
|
||||||
|
!(e.ariaSelected === 'true') &&
|
||||||
|
showDropDown
|
||||||
|
) {
|
||||||
|
setShowDropDown(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { search } = useLocation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const handleSearch = (customQuery = ''): void => {
|
||||||
|
if (liveTail === 'PLAYING') {
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: 'PAUSED',
|
||||||
|
});
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: liveTail,
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
getLogs({
|
||||||
|
q: customQuery || queryString,
|
||||||
|
limit: logLinesPerPage,
|
||||||
|
orderBy: 'timestamp',
|
||||||
|
order: 'desc',
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
...(idStart ? { idGt: idStart } : {}),
|
||||||
|
...(idEnd ? { idLt: idEnd } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
getLogsAggregate({
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
step: getStep({
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
}),
|
||||||
|
q: customQuery || queryString,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setShowDropDown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlQuery = useMemo(() => {
|
||||||
|
return new URLSearchParams(search);
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlQueryString = urlQuery.get('q');
|
||||||
|
if (urlQueryString !== null) handleSearch(urlQueryString);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={searchComponentRef} style={{ flex: 1 }}>
|
||||||
|
<Search
|
||||||
|
placeholder="Search Filter"
|
||||||
|
onFocus={(): void => setShowDropDown(true)}
|
||||||
|
value={queryString}
|
||||||
|
onChange={(e): void => {
|
||||||
|
updateQueryString(e.target.value);
|
||||||
|
}}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
/>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
{showDropDown && (
|
||||||
|
<DropDownContainer>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
onClick={(): void => setShowDropDown(false)}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseSquareOutlined />
|
||||||
|
</Button>
|
||||||
|
<SearchFields updateParsedQuery={updateParsedQuery as never} />
|
||||||
|
</DropDownContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
interface DispatchProps {
|
||||||
|
getLogs: (
|
||||||
|
props: Parameters<typeof getLogs>[0],
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
getLogsAggregate: (
|
||||||
|
props: Parameters<typeof getLogsAggregate>[0],
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
getLogs: bindActionCreators(getLogs, dispatch),
|
||||||
|
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(memo(SearchFilter));
|
12
frontend/src/container/LogsSearchFilter/styles.ts
Normal file
12
frontend/src/container/LogsSearchFilter/styles.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Card } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const DropDownContainer = styled(Card)`
|
||||||
|
top: 0.5rem;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0.8rem;
|
||||||
|
}
|
||||||
|
`;
|
76
frontend/src/container/LogsSearchFilter/useSearchParser.ts
Normal file
76
frontend/src/container/LogsSearchFilter/useSearchParser.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import history from 'lib/history';
|
||||||
|
import { parseQuery, reverseParser } from 'lib/logql';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import {
|
||||||
|
SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||||
|
SET_SEARCH_QUERY_STRING,
|
||||||
|
} from 'types/actions/logs';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
export function useSearchParser(): {
|
||||||
|
queryString: string;
|
||||||
|
parsedQuery: unknown;
|
||||||
|
updateParsedQuery: (arg0: unknown) => void;
|
||||||
|
updateQueryString: (arg0: unknown) => void;
|
||||||
|
} {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const {
|
||||||
|
searchFilter: { parsedQuery, queryString },
|
||||||
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
|
|
||||||
|
const updateQueryString = useCallback(
|
||||||
|
(updatedQueryString) => {
|
||||||
|
history.push({
|
||||||
|
pathname: history.location.pathname,
|
||||||
|
search: updatedQueryString ? `?q=${updatedQueryString}` : '',
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_SEARCH_QUERY_STRING,
|
||||||
|
payload: updatedQueryString,
|
||||||
|
});
|
||||||
|
const parsedQueryFromString = parseQuery(updatedQueryString);
|
||||||
|
if (!isEqual(parsedQuery, parsedQueryFromString)) {
|
||||||
|
dispatch({
|
||||||
|
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||||
|
payload: parsedQueryFromString,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, parsedQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryString !== null) updateQueryString(queryString);
|
||||||
|
}, [queryString, updateQueryString]);
|
||||||
|
|
||||||
|
const updateParsedQuery = useCallback(
|
||||||
|
(updatedParsedPayload) => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||||
|
payload: updatedParsedPayload,
|
||||||
|
});
|
||||||
|
const reversedParsedQuery = reverseParser(updatedParsedPayload);
|
||||||
|
if (
|
||||||
|
!isEqual(queryString, reversedParsedQuery) ||
|
||||||
|
(queryString === '' && reversedParsedQuery === '')
|
||||||
|
) {
|
||||||
|
dispatch({
|
||||||
|
type: SET_SEARCH_QUERY_STRING,
|
||||||
|
payload: reversedParsedQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, queryString],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryString,
|
||||||
|
parsedQuery,
|
||||||
|
updateParsedQuery,
|
||||||
|
updateQueryString,
|
||||||
|
};
|
||||||
|
}
|
89
frontend/src/container/LogsTable/index.tsx
Normal file
89
frontend/src/container/LogsTable/index.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import LogItem from 'components/Logs/LogItem';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
|
import { map } from 'lodash-es';
|
||||||
|
import React, { memo, useEffect } from 'react';
|
||||||
|
import { connect, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { getLogs } from 'store/actions/logs/getLogs';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import { Container, Heading } from './styles';
|
||||||
|
|
||||||
|
interface LogsTableProps {
|
||||||
|
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||||
|
}
|
||||||
|
function LogsTable({ getLogs }: LogsTableProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
searchFilter: { queryString },
|
||||||
|
logs,
|
||||||
|
logLinesPerPage,
|
||||||
|
idEnd,
|
||||||
|
idStart,
|
||||||
|
isLoading,
|
||||||
|
liveTail,
|
||||||
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
|
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (liveTail === 'STOPPED')
|
||||||
|
getLogs({
|
||||||
|
q: queryString,
|
||||||
|
limit: logLinesPerPage,
|
||||||
|
orderBy: 'timestamp',
|
||||||
|
order: 'desc',
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
...(idStart ? { idGt: idStart } : {}),
|
||||||
|
...(idEnd ? { idLt: idEnd } : {}),
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [getLogs, idEnd, idStart, logLinesPerPage, maxTime, minTime, liveTail]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner height={20} tip="Getting Logs" />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Container flex="auto">
|
||||||
|
<Heading>
|
||||||
|
<Typography.Text
|
||||||
|
style={{
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Event
|
||||||
|
</Typography.Text>
|
||||||
|
</Heading>
|
||||||
|
{Array.isArray(logs) && logs.length > 0 ? (
|
||||||
|
map(logs, (log) => <LogItem key={log.id} logData={log} />)
|
||||||
|
) : liveTail === 'PLAYING' ? (
|
||||||
|
<span>Getting live logs...</span>
|
||||||
|
) : (
|
||||||
|
<span>No log lines found</span>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
getLogs: (
|
||||||
|
props: Parameters<typeof getLogs>[0],
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
getLogs: bindActionCreators(getLogs, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(memo(LogsTable));
|
15
frontend/src/container/LogsTable/styles.ts
Normal file
15
frontend/src/container/LogsTable/styles.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Card, Col } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Container = styled(Col)`
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Heading = styled(Card)`
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
}
|
||||||
|
`;
|
@ -1,9 +1,13 @@
|
|||||||
import Table, { ColumnsType } from 'antd/lib/table';
|
import { blue } from '@ant-design/colors';
|
||||||
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Input, Space, Table } from 'antd';
|
||||||
|
import type { ColumnsType, ColumnType } from 'antd/es/table';
|
||||||
|
import type { FilterConfirmProps } from 'antd/es/table/interface';
|
||||||
import localStorageGet from 'api/browser/localstorage/get';
|
import localStorageGet from 'api/browser/localstorage/get';
|
||||||
import localStorageSet from 'api/browser/localstorage/set';
|
import localStorageSet from 'api/browser/localstorage/set';
|
||||||
import { SKIP_ONBOARDING } from 'constants/onboarding';
|
import { SKIP_ONBOARDING } from 'constants/onboarding';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import React, { useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -27,6 +31,57 @@ function Metrics(): JSX.Element {
|
|||||||
localStorageSet(SKIP_ONBOARDING, 'true');
|
localStorageSet(SKIP_ONBOARDING, 'true');
|
||||||
setSkipOnboarding(true);
|
setSkipOnboarding(true);
|
||||||
};
|
};
|
||||||
|
const handleSearch = (confirm: (param?: FilterConfirmProps) => void): void => {
|
||||||
|
confirm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilterIcon = useCallback(
|
||||||
|
({ filtered }) => (
|
||||||
|
<SearchOutlined
|
||||||
|
style={{
|
||||||
|
color: filtered ? blue[6] : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterDropdown = useCallback(
|
||||||
|
({ setSelectedKeys, selectedKeys, confirm }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Search by service"
|
||||||
|
value={selectedKeys[0]}
|
||||||
|
onChange={(e): void =>
|
||||||
|
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||||
|
}
|
||||||
|
allowClear
|
||||||
|
onPressEnter={(): void => handleSearch(confirm)}
|
||||||
|
style={{
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={(): void => handleSearch(confirm)}
|
||||||
|
icon={<SearchOutlined />}
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
width: 90,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
services.length === 0 &&
|
services.length === 0 &&
|
||||||
@ -37,21 +92,37 @@ function Metrics(): JSX.Element {
|
|||||||
return <SkipBoardModal onContinueClick={onContinueClick} />;
|
return <SkipBoardModal onContinueClick={onContinueClick} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<DataProps> = [
|
type DataIndex = keyof ServicesList;
|
||||||
{
|
|
||||||
title: 'Application',
|
const getColumnSearchProps = (
|
||||||
dataIndex: 'serviceName',
|
dataIndex: DataIndex,
|
||||||
key: 'serviceName',
|
): ColumnType<DataProps> => ({
|
||||||
|
filterDropdown,
|
||||||
|
filterIcon: FilterIcon,
|
||||||
|
onFilter: (value: string | number | boolean, record: DataProps): boolean =>
|
||||||
|
record[dataIndex]
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(value.toString().toLowerCase()),
|
||||||
render: (text: string): JSX.Element => (
|
render: (text: string): JSX.Element => (
|
||||||
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
|
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
|
||||||
<Name>{text}</Name>
|
<Name>{text}</Name>
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnsType<DataProps> = [
|
||||||
|
{
|
||||||
|
title: 'Application',
|
||||||
|
dataIndex: 'serviceName',
|
||||||
|
key: 'serviceName',
|
||||||
|
...getColumnSearchProps('serviceName'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'P99 latency (in ms)',
|
title: 'P99 latency (in ms)',
|
||||||
dataIndex: 'p99',
|
dataIndex: 'p99',
|
||||||
key: 'p99',
|
key: 'p99',
|
||||||
|
defaultSortOrder: 'descend',
|
||||||
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
|
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
|
||||||
render: (value: number): string => (value / 1000000).toFixed(2),
|
render: (value: number): string => (value / 1000000).toFixed(2),
|
||||||
},
|
},
|
||||||
|
@ -37,6 +37,14 @@ function MetricsBuilderFormula({
|
|||||||
style={{ marginBottom: '0.5rem' }}
|
style={{ marginBottom: '0.5rem' }}
|
||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
onChange={(event): void => {
|
||||||
|
handleFormulaChange({ formulaIndex, legend: event.target.value });
|
||||||
|
}}
|
||||||
|
size="middle"
|
||||||
|
defaultValue={formulaData.legend}
|
||||||
|
addonBefore="Legend Format"
|
||||||
|
/>
|
||||||
</QueryHeader>
|
</QueryHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,7 @@ function QueryBuilderQueryContainer({
|
|||||||
const handleQueryBuilderFormulaChange = ({
|
const handleQueryBuilderFormulaChange = ({
|
||||||
formulaIndex,
|
formulaIndex,
|
||||||
expression,
|
expression,
|
||||||
|
legend,
|
||||||
toggleDisable,
|
toggleDisable,
|
||||||
toggleDelete,
|
toggleDelete,
|
||||||
}: IQueryBuilderFormulaHandleChange): void => {
|
}: IQueryBuilderFormulaHandleChange): void => {
|
||||||
@ -94,9 +95,12 @@ function QueryBuilderQueryContainer({
|
|||||||
];
|
];
|
||||||
const currentIndexFormula = allFormulas[formulaIndex as number];
|
const currentIndexFormula = allFormulas[formulaIndex as number];
|
||||||
|
|
||||||
if (expression) {
|
if (expression !== undefined) {
|
||||||
currentIndexFormula.expression = expression;
|
currentIndexFormula.expression = expression;
|
||||||
}
|
}
|
||||||
|
if (legend !== undefined) {
|
||||||
|
currentIndexFormula.legend = legend;
|
||||||
|
}
|
||||||
|
|
||||||
if (toggleDisable) {
|
if (toggleDisable) {
|
||||||
currentIndexFormula.disabled = !currentIndexFormula.disabled;
|
currentIndexFormula.disabled = !currentIndexFormula.disabled;
|
||||||
|
@ -19,5 +19,6 @@ export interface IQueryBuilderFormulaHandleChange {
|
|||||||
formulaIndex: number | string;
|
formulaIndex: number | string;
|
||||||
expression?: IMetricsBuilderFormula['expression'];
|
expression?: IMetricsBuilderFormula['expression'];
|
||||||
toggleDisable?: IMetricsBuilderFormula['disabled'];
|
toggleDisable?: IMetricsBuilderFormula['disabled'];
|
||||||
|
legend?: IMetricsBuilderFormula['legend'];
|
||||||
toggleDelete?: boolean;
|
toggleDelete?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
DashboardFilled,
|
DashboardFilled,
|
||||||
DeploymentUnitOutlined,
|
DeploymentUnitOutlined,
|
||||||
LineChartOutlined,
|
LineChartOutlined,
|
||||||
|
MenuOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@ -18,10 +19,16 @@ const menus: SidebarMenu[] = [
|
|||||||
name: 'Services',
|
name: 'Services',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: AlignLeftOutlined,
|
Icon: MenuOutlined,
|
||||||
to: ROUTES.TRACE,
|
to: ROUTES.TRACE,
|
||||||
name: 'Traces',
|
name: 'Traces',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Icon: AlignLeftOutlined,
|
||||||
|
to: ROUTES.LOGS,
|
||||||
|
name: 'Logs',
|
||||||
|
tags: ['Beta'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Icon: DashboardFilled,
|
Icon: DashboardFilled,
|
||||||
to: ROUTES.ALL_DASHBOARD,
|
to: ROUTES.ALL_DASHBOARD,
|
||||||
@ -41,7 +48,6 @@ const menus: SidebarMenu[] = [
|
|||||||
to: ROUTES.SERVICE_MAP,
|
to: ROUTES.SERVICE_MAP,
|
||||||
name: 'Service Map',
|
name: 'Service Map',
|
||||||
Icon: DeploymentUnitOutlined,
|
Icon: DeploymentUnitOutlined,
|
||||||
tags: ['Beta'],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: LineChartOutlined,
|
Icon: LineChartOutlined,
|
||||||
|
@ -18,6 +18,7 @@ const breadcrumbNameMap = {
|
|||||||
[ROUTES.ERROR_DETAIL]: 'Errors',
|
[ROUTES.ERROR_DETAIL]: 'Errors',
|
||||||
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
|
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
|
||||||
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
||||||
|
[ROUTES.LOGS]: 'Logs',
|
||||||
};
|
};
|
||||||
|
|
||||||
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {
|
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {
|
||||||
|
@ -45,6 +45,10 @@ export const ServiceMapOptions: Option[] = [
|
|||||||
{ value: '5min', label: 'Last 5 min' },
|
{ value: '5min', label: 'Last 5 min' },
|
||||||
{ value: '15min', label: 'Last 15 min' },
|
{ value: '15min', label: 'Last 15 min' },
|
||||||
{ value: '30min', label: 'Last 30 min' },
|
{ value: '30min', label: 'Last 30 min' },
|
||||||
|
{ value: '1hr', label: 'Last 1 hour' },
|
||||||
|
{ value: '6hr', label: 'Last 6 hour' },
|
||||||
|
{ value: '1day', label: 'Last 1 day' },
|
||||||
|
{ value: '1week', label: 'Last 1 week' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getDefaultOption = (route: string): Time => {
|
export const getDefaultOption = (route: string): Time => {
|
||||||
|
209
frontend/src/lib/__fixtures__/logql.ts
Normal file
209
frontend/src/lib/__fixtures__/logql.ts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
/* eslint no-useless-escape: 0 */
|
||||||
|
|
||||||
|
const logqlQueries = [
|
||||||
|
{
|
||||||
|
query: "OPERATION in ('bcd','xy\\'z') AND FULLTEXT contains 'helloxyz'", // Query with IN
|
||||||
|
splitterQuery: [
|
||||||
|
'OPERATION',
|
||||||
|
'in',
|
||||||
|
"('bcd','xy\\'z')",
|
||||||
|
'AND',
|
||||||
|
'FULLTEXT',
|
||||||
|
'contains',
|
||||||
|
"'helloxyz'",
|
||||||
|
],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'OPERATION' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'in' },
|
||||||
|
{ type: 'QUERY_VALUE', value: ['bcd', "xy\\'z"] },
|
||||||
|
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||||
|
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||||
|
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: "OPERATION in ('bcd') and FULLTEXT contains 'helloxyz'", // Query with IN
|
||||||
|
splitterQuery: [
|
||||||
|
'OPERATION',
|
||||||
|
'in',
|
||||||
|
"('bcd')",
|
||||||
|
'and',
|
||||||
|
'FULLTEXT',
|
||||||
|
'contains',
|
||||||
|
"'helloxyz'",
|
||||||
|
],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'OPERATION' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'in' },
|
||||||
|
{ type: 'QUERY_VALUE', value: ['bcd'] },
|
||||||
|
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
|
||||||
|
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||||
|
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: "OPERATION in ('bcd','xyz') AND FULLTEXT contains 'helloxyz'", // Query with IN
|
||||||
|
splitterQuery: [
|
||||||
|
'OPERATION',
|
||||||
|
'in',
|
||||||
|
"('bcd','xyz')",
|
||||||
|
'AND',
|
||||||
|
'FULLTEXT',
|
||||||
|
'contains',
|
||||||
|
"'helloxyz'",
|
||||||
|
],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'OPERATION' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'in' },
|
||||||
|
{ type: 'QUERY_VALUE', value: ['bcd', 'xyz'] },
|
||||||
|
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||||
|
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||||
|
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: "status gte 200 AND FULLTEXT contains 'helloxyz'",
|
||||||
|
splitterQuery: [
|
||||||
|
'status',
|
||||||
|
'gte',
|
||||||
|
'200',
|
||||||
|
'AND',
|
||||||
|
'FULLTEXT',
|
||||||
|
'contains',
|
||||||
|
"'helloxyz'",
|
||||||
|
],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'status' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'gte' },
|
||||||
|
{ type: 'QUERY_VALUE', value: '200' },
|
||||||
|
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||||
|
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||||
|
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: "service IN ('Hello (\\'World')", // Query with quotes and brackets
|
||||||
|
splitterQuery: ['service', 'IN', "('Hello (\\'World')"],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'service' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'IN' },
|
||||||
|
{ type: 'QUERY_VALUE', value: ["Hello (\\'World"] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: "service IN ('Hello (\\'World') AND FULLTEXT contains 'ola'", // Query with full text as key pair
|
||||||
|
splitterQuery: [
|
||||||
|
'service',
|
||||||
|
'IN',
|
||||||
|
"('Hello (\\'World')",
|
||||||
|
'AND',
|
||||||
|
'FULLTEXT',
|
||||||
|
'contains',
|
||||||
|
"'ola'",
|
||||||
|
],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'service' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'IN' },
|
||||||
|
{ type: 'QUERY_VALUE', value: ["Hello (\\'World"] },
|
||||||
|
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||||
|
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||||
|
{ type: 'QUERY_VALUE', value: "'ola'" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: 'id lt 100 and id gt 50 and code lte 500 and code gte 400', // Query with numbers
|
||||||
|
splitterQuery: [
|
||||||
|
'id',
|
||||||
|
'lt',
|
||||||
|
'100',
|
||||||
|
'and',
|
||||||
|
'id',
|
||||||
|
'gt',
|
||||||
|
'50',
|
||||||
|
'and',
|
||||||
|
'code',
|
||||||
|
'lte',
|
||||||
|
'500',
|
||||||
|
'and',
|
||||||
|
'code',
|
||||||
|
'gte',
|
||||||
|
'400',
|
||||||
|
],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'id' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'lt' },
|
||||||
|
{ type: 'QUERY_VALUE', value: '100' },
|
||||||
|
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
|
||||||
|
{ type: 'QUERY_KEY', value: 'id' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'gt' },
|
||||||
|
{ type: 'QUERY_VALUE', value: '50' },
|
||||||
|
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
|
||||||
|
{ type: 'QUERY_KEY', value: 'code' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'lte' },
|
||||||
|
{ type: 'QUERY_VALUE', value: '500' },
|
||||||
|
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
|
||||||
|
{ type: 'QUERY_KEY', value: 'code' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'gte' },
|
||||||
|
{ type: 'QUERY_VALUE', value: '400' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: 'FULLTEXT contains \'Hello, "World"\'', // Full text with Quotes
|
||||||
|
splitterQuery: ['FULLTEXT', 'contains', `'Hello, "World"'`],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||||
|
{ type: 'QUERY_VALUE', value: `'Hello, "World"'` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
query: "service NIN ('name > 100') AND length gt 100", // Characters inside string
|
||||||
|
splitterQuery: [
|
||||||
|
'service',
|
||||||
|
'NIN',
|
||||||
|
"('name > 100')",
|
||||||
|
'AND',
|
||||||
|
'length',
|
||||||
|
'gt',
|
||||||
|
'100',
|
||||||
|
],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'service' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'NIN' },
|
||||||
|
{ type: 'QUERY_VALUE', value: ['name > 100'] },
|
||||||
|
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||||
|
{ type: 'QUERY_KEY', value: 'length' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'gt' },
|
||||||
|
{ type: 'QUERY_VALUE', value: '100' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Template
|
||||||
|
{
|
||||||
|
query: '',
|
||||||
|
splitterQuery: [],
|
||||||
|
parsedQuery: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const specialLogQLQueries = [
|
||||||
|
{
|
||||||
|
query: 'key IN 22', // Fulltext
|
||||||
|
splitterQuery: ['key', 'IN', '22'],
|
||||||
|
parsedQuery: [
|
||||||
|
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||||
|
{ type: 'QUERY_OPERATOR', value: 'CONTAINS' },
|
||||||
|
{ type: 'QUERY_VALUE', value: 'key IN 22' },
|
||||||
|
],
|
||||||
|
reverseParsed: "FULLTEXT CONTAINS 'key IN 22'",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { logqlQueries, specialLogQLQueries };
|
15
frontend/src/lib/__tests__/logql/parser.test.ts
Normal file
15
frontend/src/lib/__tests__/logql/parser.test.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { logqlQueries } from 'lib/__fixtures__/logql';
|
||||||
|
import parser from 'lib/logql/parser';
|
||||||
|
|
||||||
|
describe('lib/logql/parser', () => {
|
||||||
|
test('parse valid queries', () => {
|
||||||
|
logqlQueries.forEach((queryObject) => {
|
||||||
|
try {
|
||||||
|
parser(queryObject.query);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
expect(parser(queryObject.query)).toEqual(queryObject.parsedQuery);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
14
frontend/src/lib/__tests__/logql/reverseParser.test.ts
Normal file
14
frontend/src/lib/__tests__/logql/reverseParser.test.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { logqlQueries } from 'lib/__fixtures__/logql';
|
||||||
|
import { reverseParser } from 'lib/logql/reverseParser';
|
||||||
|
|
||||||
|
describe('lib/logql/reverseParser', () => {
|
||||||
|
test('reverse parse valid queries', () => {
|
||||||
|
logqlQueries.forEach((queryObject) => {
|
||||||
|
try {
|
||||||
|
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
10
frontend/src/lib/__tests__/logql/splitter.test.ts
Normal file
10
frontend/src/lib/__tests__/logql/splitter.test.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { logqlQueries } from 'lib/__fixtures__/logql';
|
||||||
|
import { splitter } from 'lib/logql/splitter';
|
||||||
|
|
||||||
|
describe('lib/logql/splitter', () => {
|
||||||
|
test('splitter valid quereies', () => {
|
||||||
|
logqlQueries.forEach((queryObject) => {
|
||||||
|
expect(splitter(queryObject.query)).toEqual(queryObject.splitterQuery);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
7
frontend/src/lib/logql/errors/ConvertToFullText.ts
Normal file
7
frontend/src/lib/logql/errors/ConvertToFullText.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export class ErrorConvertToFullText extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ErrorConvertToFullText';
|
||||||
|
Object.setPrototypeOf(this, ErrorConvertToFullText.prototype);
|
||||||
|
}
|
||||||
|
}
|
7
frontend/src/lib/logql/errors/InvalidQueryPair.ts
Normal file
7
frontend/src/lib/logql/errors/InvalidQueryPair.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export class ErrorInvalidQueryPair extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ErrorInvalidQueryPair';
|
||||||
|
Object.setPrototypeOf(this, ErrorInvalidQueryPair.prototype);
|
||||||
|
}
|
||||||
|
}
|
2
frontend/src/lib/logql/errors/index.ts
Normal file
2
frontend/src/lib/logql/errors/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './ConvertToFullText';
|
||||||
|
export * from './InvalidQueryPair';
|
4
frontend/src/lib/logql/index.ts
Normal file
4
frontend/src/lib/logql/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './parser';
|
||||||
|
export * from './reverseParser';
|
||||||
|
export * from './splitter';
|
||||||
|
export * from './tokens';
|
152
frontend/src/lib/logql/parser.ts
Normal file
152
frontend/src/lib/logql/parser.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import {
|
||||||
|
ErrorConvertToFullText,
|
||||||
|
ErrorInvalidQueryPair,
|
||||||
|
} from 'lib/logql/errors';
|
||||||
|
import splitter from 'lib/logql/splitter';
|
||||||
|
import {
|
||||||
|
ConditionalOperators,
|
||||||
|
QueryOperatorsMultiVal,
|
||||||
|
QueryOperatorsSingleVal,
|
||||||
|
QueryTypes,
|
||||||
|
} from 'lib/logql/tokens';
|
||||||
|
|
||||||
|
const validateMultiValue = (queryToken: string): boolean => {
|
||||||
|
const queryValues = [];
|
||||||
|
let start;
|
||||||
|
let isQuoteStart = false;
|
||||||
|
if (queryToken[0] === '(' && queryToken[queryToken.length - 1] === ')') {
|
||||||
|
for (let idx = 1; idx < queryToken.length - 1; idx += 1) {
|
||||||
|
if (queryToken[idx] === "'") {
|
||||||
|
if (queryToken[idx - 1] === '\\') {
|
||||||
|
// skip
|
||||||
|
} else if (isQuoteStart) {
|
||||||
|
isQuoteStart = false;
|
||||||
|
queryValues.push(queryToken.slice(start, idx));
|
||||||
|
} else {
|
||||||
|
isQuoteStart = true;
|
||||||
|
start = idx + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return queryValues;
|
||||||
|
};
|
||||||
|
export const parseQuery = (queryString) => {
|
||||||
|
let parsedRaw = [];
|
||||||
|
const generateQuery = (queryToken) => {
|
||||||
|
const prevToken = parsedRaw[parsedRaw.length - 1];
|
||||||
|
|
||||||
|
// Is a QUERY_KEY
|
||||||
|
if (
|
||||||
|
prevToken === undefined ||
|
||||||
|
prevToken.type === QueryTypes.CONDITIONAL_OPERATOR
|
||||||
|
) {
|
||||||
|
parsedRaw.push({
|
||||||
|
type: QueryTypes.QUERY_KEY,
|
||||||
|
value: queryToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Is a QUERY_OPERATOR
|
||||||
|
else if (prevToken && prevToken.type === QueryTypes.QUERY_KEY) {
|
||||||
|
if (
|
||||||
|
Object.values({
|
||||||
|
...QueryOperatorsMultiVal,
|
||||||
|
...QueryOperatorsSingleVal,
|
||||||
|
}).find((op) => op.toLowerCase() === queryToken.toLowerCase())
|
||||||
|
)
|
||||||
|
parsedRaw.push({
|
||||||
|
type: QueryTypes.QUERY_OPERATOR,
|
||||||
|
value: queryToken,
|
||||||
|
});
|
||||||
|
else {
|
||||||
|
throw new ErrorInvalidQueryPair(
|
||||||
|
'Expected conditional operator received',
|
||||||
|
queryToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Is a QUERY_VALUE
|
||||||
|
else if (prevToken && prevToken.type === QueryTypes.QUERY_OPERATOR) {
|
||||||
|
// Check for multi value
|
||||||
|
let value = queryToken;
|
||||||
|
// if (
|
||||||
|
// typeof queryToken === 'string' &&
|
||||||
|
// queryToken.length >= 2 &&
|
||||||
|
// queryToken[0] === "'" &&
|
||||||
|
// queryToken[queryToken.length - 1] === "'"
|
||||||
|
// ) {
|
||||||
|
// value = queryToken.slice(1, queryToken.length - 1);
|
||||||
|
// }
|
||||||
|
if (
|
||||||
|
Object.values(QueryOperatorsMultiVal).some(
|
||||||
|
(operator) => operator.toLowerCase() === prevToken.value.toLowerCase(),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
value = validateMultiValue(queryToken);
|
||||||
|
if (value === false) {
|
||||||
|
throw new ErrorConvertToFullText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedRaw.push({
|
||||||
|
type: QueryTypes.QUERY_VALUE,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
} else if (prevToken && prevToken.type === QueryTypes.QUERY_VALUE) {
|
||||||
|
if (
|
||||||
|
Object.values(ConditionalOperators).find(
|
||||||
|
(op) => op.toLowerCase() === queryToken.toLowerCase(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parsedRaw.push({
|
||||||
|
type: QueryTypes.CONDITIONAL_OPERATOR,
|
||||||
|
value: queryToken,
|
||||||
|
});
|
||||||
|
else {
|
||||||
|
throw new ErrorInvalidQueryPair(
|
||||||
|
'Expected conditional operator received',
|
||||||
|
queryToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a Key
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const spaceSplittedQUery = splitter(queryString);
|
||||||
|
spaceSplittedQUery.forEach((q) => {
|
||||||
|
generateQuery(q);
|
||||||
|
});
|
||||||
|
} catch (e: Error) {
|
||||||
|
if (e instanceof ErrorInvalidQueryPair) {
|
||||||
|
//
|
||||||
|
} else if (e instanceof ErrorConvertToFullText) {
|
||||||
|
parsedRaw = [
|
||||||
|
{
|
||||||
|
type: QueryTypes.QUERY_KEY,
|
||||||
|
value: 'FULLTEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: QueryTypes.QUERY_OPERATOR,
|
||||||
|
value: 'CONTAINS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: QueryTypes.QUERY_VALUE,
|
||||||
|
value: String.raw`${queryString}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(parsedRaw);
|
||||||
|
return parsedRaw;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parseQuery;
|
25
frontend/src/lib/logql/reverseParser.ts
Normal file
25
frontend/src/lib/logql/reverseParser.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
export const reverseParser = (
|
||||||
|
parserQueryArr: { type: string; value: any }[] = [],
|
||||||
|
) => {
|
||||||
|
let queryString = '';
|
||||||
|
parserQueryArr.forEach((query) => {
|
||||||
|
if (queryString) {
|
||||||
|
queryString += ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(query.value) && query.value.length > 0) {
|
||||||
|
queryString += `(${query.value.map((val) => `'${val}'`).join(',')})`;
|
||||||
|
} else {
|
||||||
|
queryString += query.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log(queryString);
|
||||||
|
return queryString;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reverseParser;
|
52
frontend/src/lib/logql/splitter.ts
Normal file
52
frontend/src/lib/logql/splitter.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
export const splitter = (queryString: string): string[] => {
|
||||||
|
const splittedParts: string[] = [];
|
||||||
|
let start = 0;
|
||||||
|
let isBracketStart = false;
|
||||||
|
let isQuoteStart = false;
|
||||||
|
|
||||||
|
const pushPart = (idx) => {
|
||||||
|
splittedParts.push(queryString.slice(start, idx));
|
||||||
|
start = idx + 1;
|
||||||
|
};
|
||||||
|
for (let idx = 0; idx < queryString.length; idx += 1) {
|
||||||
|
const currentChar = queryString[idx];
|
||||||
|
|
||||||
|
if (currentChar === ' ') {
|
||||||
|
if (!isBracketStart && !isQuoteStart) {
|
||||||
|
pushPart(idx);
|
||||||
|
}
|
||||||
|
} else if (currentChar === '(') {
|
||||||
|
isBracketStart = true;
|
||||||
|
} else if (currentChar === ')') {
|
||||||
|
if (queryString[idx - 1] !== '\\') {
|
||||||
|
pushPart(idx + 1);
|
||||||
|
isBracketStart = false;
|
||||||
|
}
|
||||||
|
if (isQuoteStart) {
|
||||||
|
isQuoteStart = false;
|
||||||
|
}
|
||||||
|
} else if (currentChar === "'") {
|
||||||
|
if (isQuoteStart) {
|
||||||
|
if (queryString[idx - 1] !== '\\' && !isBracketStart) {
|
||||||
|
pushPart(idx + 1);
|
||||||
|
isQuoteStart = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isQuoteStart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process remaining part
|
||||||
|
if (start < queryString.length) {
|
||||||
|
pushPart(queryString.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return splittedParts.map((s) => String.raw`${s}`).filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default splitter;
|
25
frontend/src/lib/logql/tokens.ts
Normal file
25
frontend/src/lib/logql/tokens.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export const QueryOperatorsSingleVal = {
|
||||||
|
GTE: 'GTE',
|
||||||
|
GT: 'GT',
|
||||||
|
LTE: 'LTE',
|
||||||
|
LT: 'LT',
|
||||||
|
CONTAINS: 'CONTAINS',
|
||||||
|
NCONTAINS: 'NCONTAINS',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueryOperatorsMultiVal = {
|
||||||
|
IN: 'IN',
|
||||||
|
NIN: 'NIN',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConditionalOperators = {
|
||||||
|
AND: 'AND',
|
||||||
|
OR: 'OR',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueryTypes = {
|
||||||
|
QUERY_KEY: 'QUERY_KEY',
|
||||||
|
QUERY_OPERATOR: 'QUERY_OPERATOR',
|
||||||
|
QUERY_VALUE: 'QUERY_VALUE',
|
||||||
|
CONDITIONAL_OPERATOR: 'CONDITIONAL_OPERATOR',
|
||||||
|
};
|
4
frontend/src/lib/logql/types.ts
Normal file
4
frontend/src/lib/logql/types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface ILogQLParsedQueryItem {
|
||||||
|
type: string;
|
||||||
|
value: string | string[];
|
||||||
|
}
|
9
frontend/src/lib/logs/fieldSearch.ts
Normal file
9
frontend/src/lib/logs/fieldSearch.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const fieldSearchFilter = (
|
||||||
|
searchSpace = '',
|
||||||
|
currentValue = '',
|
||||||
|
): boolean => {
|
||||||
|
if (!currentValue || !searchSpace) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return searchSpace.toLowerCase().indexOf(currentValue.toLowerCase()) !== -1;
|
||||||
|
};
|
16
frontend/src/lib/logs/flatLogData.ts
Normal file
16
frontend/src/lib/logs/flatLogData.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
export function FlatLogData(log: ILog): Record<string, unknown> {
|
||||||
|
const flattenLogObject: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
Object.keys(log).forEach((key: string): void => {
|
||||||
|
if (typeof log[key as never] !== 'object') {
|
||||||
|
flattenLogObject[key] = log[key as never];
|
||||||
|
} else {
|
||||||
|
Object.keys(log[key as never]).forEach((childKey) => {
|
||||||
|
flattenLogObject[childKey] = log[key as never][childKey];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return flattenLogObject;
|
||||||
|
}
|
24
frontend/src/lib/logs/generateFilterQuery.ts
Normal file
24
frontend/src/lib/logs/generateFilterQuery.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { QueryOperatorsMultiVal } from 'lib/logql/tokens';
|
||||||
|
|
||||||
|
type Keys = keyof typeof QueryOperatorsMultiVal;
|
||||||
|
type Values = typeof QueryOperatorsMultiVal[Keys];
|
||||||
|
|
||||||
|
interface GenerateFilterQueryParams {
|
||||||
|
fieldKey: string;
|
||||||
|
fieldValue: string;
|
||||||
|
type: Values;
|
||||||
|
}
|
||||||
|
export const generateFilterQuery = ({
|
||||||
|
fieldKey,
|
||||||
|
fieldValue,
|
||||||
|
type,
|
||||||
|
}: GenerateFilterQueryParams): string => {
|
||||||
|
let generatedQueryString = `${fieldKey} ${type} `;
|
||||||
|
if (typeof fieldValue === 'number') {
|
||||||
|
generatedQueryString += `(${fieldValue})`;
|
||||||
|
} else {
|
||||||
|
generatedQueryString += `('${fieldValue}')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return generatedQueryString;
|
||||||
|
};
|
8
frontend/src/pages/Logs/index.tsx
Normal file
8
frontend/src/pages/Logs/index.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Logs from 'container/Logs';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function LogsHome(): JSX.Element {
|
||||||
|
return <Logs />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogsHome;
|
@ -76,9 +76,11 @@ export async function GetMetricQueryRange({
|
|||||||
|
|
||||||
queryData[WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME].map((formula) => {
|
queryData[WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME].map((formula) => {
|
||||||
const generatedFormulaPayload = {};
|
const generatedFormulaPayload = {};
|
||||||
|
legendMap[formula.name] = formula.legend || formula.name;
|
||||||
generatedFormulaPayload.queryName = formula.name;
|
generatedFormulaPayload.queryName = formula.name;
|
||||||
generatedFormulaPayload.expression = formula.expression;
|
generatedFormulaPayload.expression = formula.expression;
|
||||||
generatedFormulaPayload.disabled = formula.disabled;
|
generatedFormulaPayload.disabled = formula.disabled;
|
||||||
|
generatedFormulaPayload.legend = formula.legend;
|
||||||
builderQueries[formula.name] = generatedFormulaPayload;
|
builderQueries[formula.name] = generatedFormulaPayload;
|
||||||
});
|
});
|
||||||
QueryPayload.compositeMetricQuery.builderQueries = builderQueries;
|
QueryPayload.compositeMetricQuery.builderQueries = builderQueries;
|
||||||
|
17
frontend/src/store/actions/logs/addToSelectedField.ts
Normal file
17
frontend/src/store/actions/logs/addToSelectedField.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import GetSearchFields from 'api/logs/GetSearchFields';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { SET_FIELDS } from 'types/actions/logs';
|
||||||
|
|
||||||
|
export const AddToSelectedField = (): ((
|
||||||
|
dispatch: Dispatch<AppActions>,
|
||||||
|
) => void) => {
|
||||||
|
return async (dispatch): Promise<void> => {
|
||||||
|
const response = await GetSearchFields();
|
||||||
|
if (response.payload)
|
||||||
|
dispatch({
|
||||||
|
type: SET_FIELDS,
|
||||||
|
payload: response.payload,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
23
frontend/src/store/actions/logs/getFields.ts
Normal file
23
frontend/src/store/actions/logs/getFields.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import GetSearchFields from 'api/logs/GetSearchFields';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { SET_FIELDS } from 'types/actions/logs';
|
||||||
|
|
||||||
|
const IGNORED_SELECTED_FIELDS = ['timestamp'];
|
||||||
|
|
||||||
|
export const GetLogsFields = (): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||||
|
return async (dispatch): Promise<void> => {
|
||||||
|
const response = await GetSearchFields();
|
||||||
|
if (response.payload) {
|
||||||
|
dispatch({
|
||||||
|
type: SET_FIELDS,
|
||||||
|
payload: {
|
||||||
|
interesting: response.payload.interesting,
|
||||||
|
selected: response.payload.selected.filter(
|
||||||
|
(field) => !IGNORED_SELECTED_FIELDS.includes(field.name),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
34
frontend/src/store/actions/logs/getLogs.ts
Normal file
34
frontend/src/store/actions/logs/getLogs.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import GetLogs from 'api/logs/GetLogs';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { SET_LOADING, SET_LOGS } from 'types/actions/logs';
|
||||||
|
import { Props } from 'types/api/logs/getLogs';
|
||||||
|
|
||||||
|
export const getLogs = (
|
||||||
|
props: Props,
|
||||||
|
): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||||
|
return async (dispatch): Promise<void> => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOADING,
|
||||||
|
payload: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await GetLogs(props);
|
||||||
|
|
||||||
|
if (response.payload)
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOGS,
|
||||||
|
payload: response.payload,
|
||||||
|
});
|
||||||
|
else
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOGS,
|
||||||
|
payload: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOADING,
|
||||||
|
payload: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
44
frontend/src/store/actions/logs/getLogsAggregate.ts
Normal file
44
frontend/src/store/actions/logs/getLogsAggregate.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import GetLogsAggregate from 'api/logs/GetLogsAggregate';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import {
|
||||||
|
SET_LOADING_AGGREGATE,
|
||||||
|
SET_LOGS_AGGREGATE_SERIES,
|
||||||
|
} from 'types/actions/logs';
|
||||||
|
import { Props } from 'types/api/logs/getLogsAggregate';
|
||||||
|
import { ILogsAggregate } from 'types/api/logs/logAggregate';
|
||||||
|
|
||||||
|
export const getLogsAggregate = (
|
||||||
|
props: Props,
|
||||||
|
): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||||
|
return async (dispatch): Promise<void> => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOADING_AGGREGATE,
|
||||||
|
payload: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await GetLogsAggregate(props);
|
||||||
|
if (response.payload) {
|
||||||
|
const convertedArray: ILogsAggregate[] = Object.values(response.payload).map(
|
||||||
|
(data) => {
|
||||||
|
return { ...data, time: new Date(data.timestamp / 1e6) };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOGS_AGGREGATE_SERIES,
|
||||||
|
payload: convertedArray,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOGS_AGGREGATE_SERIES,
|
||||||
|
payload: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_LOADING_AGGREGATE,
|
||||||
|
payload: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
@ -3,6 +3,7 @@ import { combineReducers } from 'redux';
|
|||||||
import appReducer from './app';
|
import appReducer from './app';
|
||||||
import dashboardReducer from './dashboard';
|
import dashboardReducer from './dashboard';
|
||||||
import globalTimeReducer from './global';
|
import globalTimeReducer from './global';
|
||||||
|
import { LogsReducer } from './logs';
|
||||||
import metricsReducers from './metric';
|
import metricsReducers from './metric';
|
||||||
import { ServiceMapReducer } from './serviceMap';
|
import { ServiceMapReducer } from './serviceMap';
|
||||||
import traceReducer from './trace';
|
import traceReducer from './trace';
|
||||||
@ -16,6 +17,7 @@ const reducers = combineReducers({
|
|||||||
dashboards: dashboardReducer,
|
dashboards: dashboardReducer,
|
||||||
app: appReducer,
|
app: appReducer,
|
||||||
metrics: metricsReducers,
|
metrics: metricsReducers,
|
||||||
|
logs: LogsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppState = ReturnType<typeof reducers>;
|
export type AppState = ReturnType<typeof reducers>;
|
||||||
|
213
frontend/src/store/reducers/logs.ts
Normal file
213
frontend/src/store/reducers/logs.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { parseQuery } from 'lib/logql';
|
||||||
|
import {
|
||||||
|
ADD_SEARCH_FIELD_QUERY_STRING,
|
||||||
|
FLUSH_LOGS,
|
||||||
|
GET_FIELDS,
|
||||||
|
GET_NEXT_LOG_LINES,
|
||||||
|
GET_PREVIOUS_LOG_LINES,
|
||||||
|
LogsActions,
|
||||||
|
PUSH_LIVE_TAIL_EVENT,
|
||||||
|
RESET_ID_START_AND_END,
|
||||||
|
SET_DETAILED_LOG_DATA,
|
||||||
|
SET_FIELDS,
|
||||||
|
SET_LIVE_TAIL_START_TIME,
|
||||||
|
SET_LOADING,
|
||||||
|
SET_LOADING_AGGREGATE,
|
||||||
|
SET_LOG_LINES_PER_PAGE,
|
||||||
|
SET_LOGS,
|
||||||
|
SET_LOGS_AGGREGATE_SERIES,
|
||||||
|
SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||||
|
SET_SEARCH_QUERY_STRING,
|
||||||
|
STOP_LIVE_TAIL,
|
||||||
|
TOGGLE_LIVE_TAIL,
|
||||||
|
} from 'types/actions/logs';
|
||||||
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
const initialState: ILogsReducer = {
|
||||||
|
fields: {
|
||||||
|
interesting: [],
|
||||||
|
selected: [],
|
||||||
|
},
|
||||||
|
searchFilter: {
|
||||||
|
queryString: '',
|
||||||
|
parsedQuery: [],
|
||||||
|
},
|
||||||
|
logs: [],
|
||||||
|
logLinesPerPage: 25,
|
||||||
|
idEnd: '',
|
||||||
|
idStart: '',
|
||||||
|
isLoading: false,
|
||||||
|
isLoadingAggregate: false,
|
||||||
|
logsAggregate: [],
|
||||||
|
liveTail: 'STOPPED',
|
||||||
|
liveTailStartRange: 15,
|
||||||
|
selectedLogId: null,
|
||||||
|
detailedLog: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LogsReducer = (
|
||||||
|
state = initialState,
|
||||||
|
action: LogsActions,
|
||||||
|
): ILogsReducer => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_LOADING: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case SET_LOADING_AGGREGATE: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoadingAggregate: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case GET_FIELDS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
|
||||||
|
case SET_FIELDS: {
|
||||||
|
const newFields = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
fields: newFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case SET_SEARCH_QUERY_STRING: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
searchFilter: {
|
||||||
|
...state.searchFilter,
|
||||||
|
queryString: action.payload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case SET_SEARCH_QUERY_PARSED_PAYLOAD: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
searchFilter: {
|
||||||
|
...state.searchFilter,
|
||||||
|
parsedQuery: action.payload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ADD_SEARCH_FIELD_QUERY_STRING: {
|
||||||
|
const updatedQueryString =
|
||||||
|
state.searchFilter.queryString ||
|
||||||
|
`${
|
||||||
|
state.searchFilter.queryString && state.searchFilter.queryString.length > 0
|
||||||
|
? ' and '
|
||||||
|
: ''
|
||||||
|
}${action.payload}`;
|
||||||
|
|
||||||
|
const updatedParsedQuery = parseQuery(updatedQueryString);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
searchFilter: {
|
||||||
|
...state.searchFilter,
|
||||||
|
queryString: updatedQueryString,
|
||||||
|
parsedQuery: updatedParsedQuery,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case SET_LOGS: {
|
||||||
|
const logsData = action.payload;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
logs: logsData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case SET_LOG_LINES_PER_PAGE: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
logLinesPerPage: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case GET_PREVIOUS_LOG_LINES: {
|
||||||
|
const idStart = state.logs.length > 0 ? state.logs[0].id : '';
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
idStart,
|
||||||
|
idEnd: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case GET_NEXT_LOG_LINES: {
|
||||||
|
const idEnd =
|
||||||
|
state.logs.length > 0 ? state.logs[state.logs.length - 1].id : '';
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
idStart: '',
|
||||||
|
idEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case RESET_ID_START_AND_END: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
idEnd: '',
|
||||||
|
idStart: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case SET_LOGS_AGGREGATE_SERIES: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
logsAggregate: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case SET_DETAILED_LOG_DATA: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
detailedLog: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case TOGGLE_LIVE_TAIL: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
liveTail: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case STOP_LIVE_TAIL: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
logs: [],
|
||||||
|
liveTail: 'STOPPED',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case PUSH_LIVE_TAIL_EVENT: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
logs: action.payload.concat(state.logs).slice(0, 100),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case SET_LIVE_TAIL_START_TIME: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
liveTailStartRange: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case FLUSH_LOGS: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
logs: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogsReducer;
|
@ -1,6 +1,7 @@
|
|||||||
import { AppAction } from './app';
|
import { AppAction } from './app';
|
||||||
import { DashboardActions } from './dashboard';
|
import { DashboardActions } from './dashboard';
|
||||||
import { GlobalTimeAction } from './globalTime';
|
import { GlobalTimeAction } from './globalTime';
|
||||||
|
import { LogsActions } from './logs';
|
||||||
import { MetricsActions } from './metrics';
|
import { MetricsActions } from './metrics';
|
||||||
import { TraceActions } from './trace';
|
import { TraceActions } from './trace';
|
||||||
|
|
||||||
@ -9,6 +10,7 @@ type AppActions =
|
|||||||
| AppAction
|
| AppAction
|
||||||
| GlobalTimeAction
|
| GlobalTimeAction
|
||||||
| MetricsActions
|
| MetricsActions
|
||||||
| TraceActions;
|
| TraceActions
|
||||||
|
| LogsActions;
|
||||||
|
|
||||||
export default AppActions;
|
export default AppActions;
|
||||||
|
147
frontend/src/types/actions/logs.ts
Normal file
147
frontend/src/types/actions/logs.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// import { DBOverView } from 'types/api/metrics/getDBOverview';
|
||||||
|
// import { ExternalAverageDuration } from 'types/api/metrics/getExternalAverageDuration';
|
||||||
|
// import { ExternalError } from 'types/api/metrics/getExternalError';
|
||||||
|
// import { ExternalService } from 'types/api/metrics/getExternalService';
|
||||||
|
// import { IResourceAttributeQuery } from 'container/MetricsApplication/ResourceAttributesFilter/types';
|
||||||
|
// import { ServicesList } from 'types/api/metrics/getService';
|
||||||
|
// import { ServiceOverview } from 'types/api/metrics/getServiceOverview';
|
||||||
|
// import { TopEndPoints } from 'types/api/metrics/getTopEndPoints';
|
||||||
|
|
||||||
|
import { ILogQLParsedQueryItem } from 'lib/logql/types';
|
||||||
|
import { IFieldMoveToSelected, IFields } from 'types/api/logs/fields';
|
||||||
|
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { ILogsAggregate } from 'types/api/logs/logAggregate';
|
||||||
|
|
||||||
|
// export const GET_SERVICE_LIST_SUCCESS = 'GET_SERVICE_LIST_SUCCESS';
|
||||||
|
// export const GET_SERVICE_LIST_LOADING_START = 'GET_SERVICE_LIST_LOADING_START';
|
||||||
|
// export const GET_SERVICE_LIST_ERROR = 'GET_SERVICE_LIST_ERROR';
|
||||||
|
// export const GET_INITIAL_APPLICATION_LOADING =
|
||||||
|
// 'GET_INITIAL_APPLICATION_LOADING';
|
||||||
|
// export const GET_INITIAL_APPLICATION_ERROR = 'GET_INITIAL_APPLICATION_ERROR';
|
||||||
|
// export const GET_INTIAL_APPLICATION_DATA = 'GET_INTIAL_APPLICATION_DATA';
|
||||||
|
// export const RESET_INITIAL_APPLICATION_DATA = 'RESET_INITIAL_APPLICATION_DATA';
|
||||||
|
export const GET_FIELDS = 'LOGS_GET_FIELDS';
|
||||||
|
export const SET_FIELDS = 'LOGS_SET_FIELDS';
|
||||||
|
export const SET_SEARCH_QUERY_STRING = 'LOGS_SET_SEARCH_QUERY_STRING';
|
||||||
|
export const SET_SEARCH_QUERY_PARSED_PAYLOAD =
|
||||||
|
'LOGS_SET_SEARCH_QUERY_PARSED_PAYLOAD';
|
||||||
|
export const ADD_SEARCH_FIELD_QUERY_STRING =
|
||||||
|
'LOGS_ADD_SEARCH_FIELD_QUERY_STRING';
|
||||||
|
export const ADD_TO_SELECTED_FIELD = 'LOGS_ADD_TO_SELECTED_FIELD';
|
||||||
|
export const SET_LOGS = 'LOGS_SET_LOGS';
|
||||||
|
export const SET_LOG_LINES_PER_PAGE = 'LOGS_SET_LOG_LINES_PER_PAGE';
|
||||||
|
export const GET_NEXT_LOG_LINES = 'LOGS_GET_NEXT_LOG_LINES';
|
||||||
|
export const GET_PREVIOUS_LOG_LINES = 'LOGS_GET_PREVIOUS_LOG_LINES';
|
||||||
|
export const RESET_ID_START_AND_END = 'LOGS_RESET_ID_START_AND_END';
|
||||||
|
export const SET_LOADING = 'LOGS_SET_LOADING';
|
||||||
|
export const SET_LOADING_AGGREGATE = 'LOGS_SET_LOADING_AGGREGATE';
|
||||||
|
export const SET_LOGS_AGGREGATE_SERIES = 'LOGS_SET_LOGS_AGGREGATE_SERIES';
|
||||||
|
export const SET_DETAILED_LOG_DATA = 'LOGS_SET_DETAILED_LOG_DATA';
|
||||||
|
export const TOGGLE_LIVE_TAIL = 'LOGS_TOGGLE_LIVE_TAIL';
|
||||||
|
export const PUSH_LIVE_TAIL_EVENT = 'LOGS_PUSH_LIVE_TAIL_EVENT';
|
||||||
|
export const STOP_LIVE_TAIL = 'LOGS_STOP_LIVE_TAIL';
|
||||||
|
export const FLUSH_LOGS = 'LOGS_FLUSH_LOGS';
|
||||||
|
export const SET_LIVE_TAIL_START_TIME = 'LOGS_SET_LIVE_TAIL_START_TIME';
|
||||||
|
export interface GetFields {
|
||||||
|
type: typeof GET_FIELDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetFields {
|
||||||
|
type: typeof SET_FIELDS;
|
||||||
|
payload: IFields;
|
||||||
|
}
|
||||||
|
export interface SetSearchQueryString {
|
||||||
|
type: typeof SET_SEARCH_QUERY_STRING;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetSearchQueryParsedPayload {
|
||||||
|
type: typeof SET_SEARCH_QUERY_PARSED_PAYLOAD;
|
||||||
|
payload: ILogQLParsedQueryItem[];
|
||||||
|
}
|
||||||
|
export interface AddSearchFieldQueryString {
|
||||||
|
type: typeof ADD_SEARCH_FIELD_QUERY_STRING;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
export interface AddToSelectedField {
|
||||||
|
type: typeof ADD_TO_SELECTED_FIELD;
|
||||||
|
payload: IFieldMoveToSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateLogs {
|
||||||
|
type: typeof SET_LOGS;
|
||||||
|
payload: ILog[];
|
||||||
|
}
|
||||||
|
export interface SetLogsLinesPerPage {
|
||||||
|
type: typeof SET_LOG_LINES_PER_PAGE;
|
||||||
|
payload: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreviousLogsLines {
|
||||||
|
type: typeof GET_PREVIOUS_LOG_LINES;
|
||||||
|
}
|
||||||
|
export interface NextLogsLines {
|
||||||
|
type: typeof GET_NEXT_LOG_LINES;
|
||||||
|
}
|
||||||
|
export interface ResetIdStartAndEnd {
|
||||||
|
type: typeof RESET_ID_START_AND_END;
|
||||||
|
}
|
||||||
|
export interface SetLoading {
|
||||||
|
type: typeof SET_LOADING;
|
||||||
|
payload: boolean;
|
||||||
|
}
|
||||||
|
export interface SetLoadingAggregate {
|
||||||
|
type: typeof SET_LOADING_AGGREGATE;
|
||||||
|
payload: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetLogsAggregateSeries {
|
||||||
|
type: typeof SET_LOGS_AGGREGATE_SERIES;
|
||||||
|
payload: ILogsAggregate[];
|
||||||
|
}
|
||||||
|
export interface SetDetailedLogData {
|
||||||
|
type: typeof SET_DETAILED_LOG_DATA;
|
||||||
|
payload: ILog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleLiveTail {
|
||||||
|
type: typeof TOGGLE_LIVE_TAIL;
|
||||||
|
payload: TLogsLiveTailState;
|
||||||
|
}
|
||||||
|
export interface PushLiveTailEvent {
|
||||||
|
type: typeof PUSH_LIVE_TAIL_EVENT;
|
||||||
|
payload: ILog[];
|
||||||
|
}
|
||||||
|
export interface StopLiveTail {
|
||||||
|
type: typeof STOP_LIVE_TAIL;
|
||||||
|
}
|
||||||
|
export interface FlushLogs {
|
||||||
|
type: typeof FLUSH_LOGS;
|
||||||
|
}
|
||||||
|
export interface SetLiveTailStartTime {
|
||||||
|
type: typeof SET_LIVE_TAIL_START_TIME;
|
||||||
|
payload: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogsActions =
|
||||||
|
| GetFields
|
||||||
|
| SetFields
|
||||||
|
| SetSearchQueryString
|
||||||
|
| SetSearchQueryParsedPayload
|
||||||
|
| AddSearchFieldQueryString
|
||||||
|
| AddToSelectedField
|
||||||
|
| UpdateLogs
|
||||||
|
| SetLogsLinesPerPage
|
||||||
|
| PreviousLogsLines
|
||||||
|
| NextLogsLines
|
||||||
|
| ResetIdStartAndEnd
|
||||||
|
| SetLoading
|
||||||
|
| SetLoadingAggregate
|
||||||
|
| SetLogsAggregateSeries
|
||||||
|
| SetDetailedLogData
|
||||||
|
| ToggleLiveTail
|
||||||
|
| PushLiveTailEvent
|
||||||
|
| StopLiveTail
|
||||||
|
| FlushLogs
|
||||||
|
| SetLiveTailStartTime;
|
@ -30,8 +30,7 @@ export interface IBuilderQuery
|
|||||||
extends Omit<
|
extends Omit<
|
||||||
IMetricQuery,
|
IMetricQuery,
|
||||||
'aggregateOperator' | 'legend' | 'metricName' | 'tagFilters'
|
'aggregateOperator' | 'legend' | 'metricName' | 'tagFilters'
|
||||||
>,
|
> {
|
||||||
Omit<IFormulaQuery, 'expression'> {
|
|
||||||
aggregateOperator: EAggregateOperator | undefined;
|
aggregateOperator: EAggregateOperator | undefined;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -71,6 +71,7 @@ export interface IMetricsBuilderFormula {
|
|||||||
expression: string;
|
expression: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
legend: string;
|
||||||
}
|
}
|
||||||
export interface IMetricsBuilderQuery {
|
export interface IMetricsBuilderQuery {
|
||||||
aggregateOperator: EAggregateOperator;
|
aggregateOperator: EAggregateOperator;
|
||||||
|
4
frontend/src/types/api/logs/addToSelectedFields.ts
Normal file
4
frontend/src/types/api/logs/addToSelectedFields.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { IFieldMoveToSelected } from './fields';
|
||||||
|
|
||||||
|
export type Props = IFieldMoveToSelected;
|
||||||
|
export type PayloadProps = IFieldMoveToSelected;
|
16
frontend/src/types/api/logs/fields.ts
Normal file
16
frontend/src/types/api/logs/fields.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export interface IField {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
dataType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFields {
|
||||||
|
selected: ISelectedFields[];
|
||||||
|
interesting: IInterestingFields[];
|
||||||
|
}
|
||||||
|
export type ISelectedFields = IField;
|
||||||
|
export type IInterestingFields = IField;
|
||||||
|
|
||||||
|
export type IFieldMoveToSelected = IField & {
|
||||||
|
selected: boolean;
|
||||||
|
};
|
13
frontend/src/types/api/logs/getLogs.ts
Normal file
13
frontend/src/types/api/logs/getLogs.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ILog } from './log';
|
||||||
|
|
||||||
|
export type PayloadProps = ILog[];
|
||||||
|
export type Props = {
|
||||||
|
q: string;
|
||||||
|
limit: number;
|
||||||
|
orderBy: string;
|
||||||
|
order: string;
|
||||||
|
idGt?: string;
|
||||||
|
idLt?: string;
|
||||||
|
timestampStart?: number;
|
||||||
|
timestampEnd?: number;
|
||||||
|
};
|
15
frontend/src/types/api/logs/getLogsAggregate.ts
Normal file
15
frontend/src/types/api/logs/getLogsAggregate.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export type PayloadProps = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
timestamp: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
export type Props = {
|
||||||
|
timestampStart: number;
|
||||||
|
timestampEnd: number;
|
||||||
|
step: number;
|
||||||
|
q?: string;
|
||||||
|
idGt?: string;
|
||||||
|
idLt?: string;
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user