mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-18 16:07:10 +00:00
Merge branch 'main' of https://github.com/SigNoz/signoz into fix/extract-query-params
This commit is contained in:
commit
30bf3a53f5
@ -40,7 +40,7 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
schema-migrator-sync:
|
schema-migrator-sync:
|
||||||
image: signoz/signoz-schema-migrator:v0.128.2
|
image: signoz/signoz-schema-migrator:v0.129.0
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@ -53,7 +53,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
schema-migrator-async:
|
schema-migrator-async:
|
||||||
image: signoz/signoz-schema-migrator:v0.128.2
|
image: signoz/signoz-schema-migrator:v0.129.0
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
29
.devenv/docker/signoz-otel-collector/compose.yaml
Normal file
29
.devenv/docker/signoz-otel-collector/compose.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
services:
|
||||||
|
signoz-otel-collector:
|
||||||
|
image: signoz/signoz-otel-collector:v0.128.2
|
||||||
|
container_name: signoz-otel-collector-dev
|
||||||
|
command:
|
||||||
|
- --config=/etc/otel-collector-config.yaml
|
||||||
|
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||||
|
volumes:
|
||||||
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
|
environment:
|
||||||
|
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||||
|
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||||
|
ports:
|
||||||
|
- "4317:4317" # OTLP gRPC receiver
|
||||||
|
- "4318:4318" # OTLP HTTP receiver
|
||||||
|
- "13133:13133" # health check extension
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- wget
|
||||||
|
- --spider
|
||||||
|
- -q
|
||||||
|
- localhost:13133
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
endpoint: 0.0.0.0:4317
|
||||||
|
http:
|
||||||
|
endpoint: 0.0.0.0:4318
|
||||||
|
prometheus:
|
||||||
|
config:
|
||||||
|
global:
|
||||||
|
scrape_interval: 60s
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: otel-collector
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- localhost:8888
|
||||||
|
labels:
|
||||||
|
job_name: otel-collector
|
||||||
|
|
||||||
|
processors:
|
||||||
|
batch:
|
||||||
|
send_batch_size: 10000
|
||||||
|
send_batch_max_size: 11000
|
||||||
|
timeout: 10s
|
||||||
|
resourcedetection:
|
||||||
|
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||||
|
detectors: [env, system]
|
||||||
|
timeout: 2s
|
||||||
|
signozspanmetrics/delta:
|
||||||
|
metrics_exporter: signozclickhousemetrics
|
||||||
|
metrics_flush_interval: 60s
|
||||||
|
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||||
|
dimensions_cache_size: 100000
|
||||||
|
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
|
||||||
|
enable_exp_histogram: true
|
||||||
|
dimensions:
|
||||||
|
- name: service.namespace
|
||||||
|
default: default
|
||||||
|
- name: deployment.environment
|
||||||
|
default: default
|
||||||
|
# This is added to ensure the uniqueness of the timeseries
|
||||||
|
# Otherwise, identical timeseries produced by multiple replicas of
|
||||||
|
# collectors result in incorrect APM metrics
|
||||||
|
- name: signoz.collector.id
|
||||||
|
- name: service.version
|
||||||
|
- name: browser.platform
|
||||||
|
- name: browser.mobile
|
||||||
|
- name: k8s.cluster.name
|
||||||
|
- name: k8s.node.name
|
||||||
|
- name: k8s.namespace.name
|
||||||
|
- name: host.name
|
||||||
|
- name: host.type
|
||||||
|
- name: container.name
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
health_check:
|
||||||
|
endpoint: 0.0.0.0:13133
|
||||||
|
pprof:
|
||||||
|
endpoint: 0.0.0.0:1777
|
||||||
|
|
||||||
|
exporters:
|
||||||
|
clickhousetraces:
|
||||||
|
datasource: tcp://host.docker.internal:9000/signoz_traces
|
||||||
|
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
|
use_new_schema: true
|
||||||
|
signozclickhousemetrics:
|
||||||
|
dsn: tcp://host.docker.internal:9000/signoz_metrics
|
||||||
|
clickhouselogsexporter:
|
||||||
|
dsn: tcp://host.docker.internal:9000/signoz_logs
|
||||||
|
timeout: 10s
|
||||||
|
use_new_schema: true
|
||||||
|
|
||||||
|
service:
|
||||||
|
telemetry:
|
||||||
|
logs:
|
||||||
|
encoding: json
|
||||||
|
extensions:
|
||||||
|
- health_check
|
||||||
|
- pprof
|
||||||
|
pipelines:
|
||||||
|
traces:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [signozspanmetrics/delta, batch]
|
||||||
|
exporters: [clickhousetraces]
|
||||||
|
metrics:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [signozclickhousemetrics]
|
||||||
|
metrics/prometheus:
|
||||||
|
receivers: [prometheus]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [signozclickhousemetrics]
|
||||||
|
logs:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [clickhouselogsexporter]
|
||||||
11
Makefile
11
Makefile
@ -61,6 +61,17 @@ devenv-postgres: ## Run postgres in devenv
|
|||||||
@cd .devenv/docker/postgres; \
|
@cd .devenv/docker/postgres; \
|
||||||
docker compose -f compose.yaml up -d
|
docker compose -f compose.yaml up -d
|
||||||
|
|
||||||
|
.PHONY: devenv-signoz-otel-collector
|
||||||
|
devenv-signoz-otel-collector: ## Run signoz-otel-collector in devenv (requires clickhouse to be running)
|
||||||
|
@cd .devenv/docker/signoz-otel-collector; \
|
||||||
|
docker compose -f compose.yaml up -d
|
||||||
|
|
||||||
|
.PHONY: devenv-up
|
||||||
|
devenv-up: devenv-clickhouse devenv-signoz-otel-collector ## Start both clickhouse and signoz-otel-collector for local development
|
||||||
|
@echo "Development environment is ready!"
|
||||||
|
@echo " - ClickHouse: http://localhost:8123"
|
||||||
|
@echo " - Signoz OTel Collector: grpc://localhost:4317, http://localhost:4318"
|
||||||
|
|
||||||
##############################################################
|
##############################################################
|
||||||
# go commands
|
# go commands
|
||||||
##############################################################
|
##############################################################
|
||||||
|
|||||||
@ -121,6 +121,8 @@ telemetrystore:
|
|||||||
timeout_before_checking_execution_speed: 0
|
timeout_before_checking_execution_speed: 0
|
||||||
max_bytes_to_read: 0
|
max_bytes_to_read: 0
|
||||||
max_result_rows: 0
|
max_result_rows: 0
|
||||||
|
ignore_data_skipping_indices: ""
|
||||||
|
secondary_indices_enable_bulk_filtering: false
|
||||||
|
|
||||||
##################### Prometheus #####################
|
##################### Prometheus #####################
|
||||||
prometheus:
|
prometheus:
|
||||||
|
|||||||
@ -174,7 +174,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.91.0
|
image: signoz/signoz:v0.92.1
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
@ -207,7 +207,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.128.2
|
image: signoz/signoz-otel-collector:v0.129.0
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
- --manager-config=/etc/manager-config.yaml
|
- --manager-config=/etc/manager-config.yaml
|
||||||
@ -231,7 +231,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.128.2
|
image: signoz/signoz-schema-migrator:v0.129.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@ -115,7 +115,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.91.0
|
image: signoz/signoz:v0.92.1
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
@ -148,7 +148,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.128.2
|
image: signoz/signoz-otel-collector:v0.129.0
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
- --manager-config=/etc/manager-config.yaml
|
- --manager-config=/etc/manager-config.yaml
|
||||||
@ -174,7 +174,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.128.2
|
image: signoz/signoz-schema-migrator:v0.129.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@ -177,7 +177,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.91.0}
|
image: signoz/signoz:${VERSION:-v0.92.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@ -211,7 +211,7 @@ services:
|
|||||||
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
@ -237,7 +237,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
schema-migrator-sync:
|
schema-migrator-sync:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@ -248,7 +248,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
schema-migrator-async:
|
schema-migrator-async:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
@ -110,7 +110,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.91.0}
|
image: signoz/signoz:${VERSION:-v0.92.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@ -143,7 +143,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
@ -165,7 +165,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
schema-migrator-sync:
|
schema-migrator-sync:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@ -177,7 +177,7 @@ services:
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
schema-migrator-async:
|
schema-migrator-async:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
@ -44,20 +44,35 @@ Before diving in, make sure you have these tools installed:
|
|||||||
|
|
||||||
SigNoz has three main components: Clickhouse, Backend, and Frontend. Let's set them up one by one.
|
SigNoz has three main components: Clickhouse, Backend, and Frontend. Let's set them up one by one.
|
||||||
|
|
||||||
### 1. Setting up Clickhouse
|
### 1. Setting up ClickHouse
|
||||||
|
|
||||||
First, we need to get Clickhouse running:
|
First, we need to get ClickHouse running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make devenv-clickhouse
|
make devenv-clickhouse
|
||||||
```
|
```
|
||||||
|
|
||||||
This command:
|
This command:
|
||||||
- Starts Clickhouse in a single-shard, single-replica cluster
|
- Starts ClickHouse in a single-shard, single-replica cluster
|
||||||
- Sets up Zookeeper
|
- Sets up Zookeeper
|
||||||
- Runs the latest schema migrations
|
- Runs the latest schema migrations
|
||||||
|
|
||||||
### 2. Starting the Backend
|
### 2. Setting up SigNoz OpenTelemetry Collector
|
||||||
|
|
||||||
|
Next, start the OpenTelemetry Collector to receive telemetry data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make devenv-signoz-otel-collector
|
||||||
|
```
|
||||||
|
|
||||||
|
This command:
|
||||||
|
- Starts the SigNoz OpenTelemetry Collector
|
||||||
|
- Listens on port 4317 (gRPC) and 4318 (HTTP) for incoming telemetry data
|
||||||
|
- Forwards data to ClickHouse for storage
|
||||||
|
|
||||||
|
> 💡 **Quick Setup**: Use `make devenv-up` to start both ClickHouse and OTel Collector together
|
||||||
|
|
||||||
|
### 3. Starting the Backend
|
||||||
|
|
||||||
1. Run the backend server:
|
1. Run the backend server:
|
||||||
```bash
|
```bash
|
||||||
@ -73,7 +88,7 @@ This command:
|
|||||||
|
|
||||||
> 💡 **Tip**: The API server runs at `http://localhost:8080/` by default
|
> 💡 **Tip**: The API server runs at `http://localhost:8080/` by default
|
||||||
|
|
||||||
### 3. Setting up the Frontend
|
### 4. Setting up the Frontend
|
||||||
|
|
||||||
1. Navigate to the frontend directory:
|
1. Navigate to the frontend directory:
|
||||||
```bash
|
```bash
|
||||||
@ -98,3 +113,25 @@ This command:
|
|||||||
> 💡 **Tip**: `yarn dev` will automatically rebuild when you make changes to the code
|
> 💡 **Tip**: `yarn dev` will automatically rebuild when you make changes to the code
|
||||||
|
|
||||||
Now you're all set to start developing! Happy coding! 🎉
|
Now you're all set to start developing! Happy coding! 🎉
|
||||||
|
|
||||||
|
## Verifying Your Setup
|
||||||
|
To verify everything is working correctly:
|
||||||
|
|
||||||
|
1. **Check ClickHouse**: `curl http://localhost:8123/ping` (should return "Ok.")
|
||||||
|
2. **Check OTel Collector**: `curl http://localhost:13133` (should return health status)
|
||||||
|
3. **Check Backend**: `curl http://localhost:8080/api/v1/health` (should return `{"status":"ok"}`)
|
||||||
|
4. **Check Frontend**: Open `http://localhost:3301` in your browser
|
||||||
|
|
||||||
|
## How to send test data?
|
||||||
|
|
||||||
|
You can now send telemetry data to your local SigNoz instance:
|
||||||
|
|
||||||
|
- **OTLP gRPC**: `localhost:4317`
|
||||||
|
- **OTLP HTTP**: `localhost:4318`
|
||||||
|
|
||||||
|
For example, using `curl` to send a test trace:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:4318/v1/traces \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"test-service"}}]},"scopeSpans":[{"spans":[{"traceId":"12345678901234567890123456789012","spanId":"1234567890123456","name":"test-span","startTimeUnixNano":"1609459200000000000","endTimeUnixNano":"1609459201000000000"}]}]}]}'
|
||||||
|
```
|
||||||
|
|||||||
@ -46,5 +46,8 @@
|
|||||||
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||||
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
||||||
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
||||||
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
|
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
|
||||||
|
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||||
|
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
|
||||||
|
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,5 +69,8 @@
|
|||||||
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
|
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
|
||||||
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
|
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
|
||||||
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
|
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
|
||||||
"API_MONITORING": "SigNoz | External APIs"
|
"API_MONITORING": "SigNoz | External APIs",
|
||||||
|
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||||
|
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
|
||||||
|
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import MessagingQueues from 'pages/MessagingQueues';
|
import MessagingQueues from 'pages/MessagingQueues';
|
||||||
|
import MeterExplorer from 'pages/MeterExplorer';
|
||||||
import { RouteProps } from 'react-router-dom';
|
import { RouteProps } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -434,6 +435,28 @@ const routes: AppRoutes[] = [
|
|||||||
key: 'METRICS_EXPLORER_VIEWS',
|
key: 'METRICS_EXPLORER_VIEWS',
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: ROUTES.METER_EXPLORER_BASE,
|
||||||
|
exact: true,
|
||||||
|
component: MeterExplorer,
|
||||||
|
key: 'METER_EXPLORER_BASE',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.METER_EXPLORER,
|
||||||
|
exact: true,
|
||||||
|
component: MeterExplorer,
|
||||||
|
key: 'METER_EXPLORER',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.METER_EXPLORER_VIEWS,
|
||||||
|
exact: true,
|
||||||
|
component: MeterExplorer,
|
||||||
|
key: 'METER_EXPLORER_VIEWS',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.API_MONITORING,
|
path: ROUTES.API_MONITORING,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export const getAggregateAttribute = async ({
|
|||||||
aggregateOperator,
|
aggregateOperator,
|
||||||
searchText,
|
searchText,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
source,
|
||||||
}: IGetAggregateAttributePayload): Promise<
|
}: IGetAggregateAttributePayload): Promise<
|
||||||
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||||
> => {
|
> => {
|
||||||
@ -27,7 +28,7 @@ export const getAggregateAttribute = async ({
|
|||||||
`/autocomplete/aggregate_attributes?${createQueryParams({
|
`/autocomplete/aggregate_attributes?${createQueryParams({
|
||||||
aggregateOperator,
|
aggregateOperator,
|
||||||
searchText,
|
searchText,
|
||||||
dataSource,
|
dataSource: source === 'meter' ? 'meter' : dataSource,
|
||||||
})}`,
|
})}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export const getKeySuggestions = (
|
|||||||
metricName = '',
|
metricName = '',
|
||||||
fieldContext = '',
|
fieldContext = '',
|
||||||
fieldDataType = '',
|
fieldDataType = '',
|
||||||
|
signalSource = '',
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const encodedSignal = encodeURIComponent(signal);
|
const encodedSignal = encodeURIComponent(signal);
|
||||||
@ -21,8 +22,9 @@ export const getKeySuggestions = (
|
|||||||
const encodedMetricName = encodeURIComponent(metricName);
|
const encodedMetricName = encodeURIComponent(metricName);
|
||||||
const encodedFieldContext = encodeURIComponent(fieldContext);
|
const encodedFieldContext = encodeURIComponent(fieldContext);
|
||||||
const encodedFieldDataType = encodeURIComponent(fieldDataType);
|
const encodedFieldDataType = encodeURIComponent(fieldDataType);
|
||||||
|
const encodedSource = encodeURIComponent(signalSource);
|
||||||
|
|
||||||
return axios.get(
|
return axios.get(
|
||||||
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}`,
|
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,13 +8,15 @@ import {
|
|||||||
export const getValueSuggestions = (
|
export const getValueSuggestions = (
|
||||||
props: QueryKeyValueRequestProps,
|
props: QueryKeyValueRequestProps,
|
||||||
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
|
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
|
||||||
const { signal, key, searchText } = props;
|
const { signal, key, searchText, signalSource, metricName } = props;
|
||||||
|
|
||||||
const encodedSignal = encodeURIComponent(signal);
|
const encodedSignal = encodeURIComponent(signal);
|
||||||
const encodedKey = encodeURIComponent(key);
|
const encodedKey = encodeURIComponent(key);
|
||||||
|
const encodedMetricName = encodeURIComponent(metricName || '');
|
||||||
const encodedSearchText = encodeURIComponent(searchText);
|
const encodedSearchText = encodeURIComponent(searchText);
|
||||||
|
const encodedSource = encodeURIComponent(signalSource || '');
|
||||||
|
|
||||||
return axios.get(
|
return axios.get(
|
||||||
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}`,
|
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,6 @@ import { AllViewsProps } from 'types/api/saveViews/types';
|
|||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export const getAllViews = (
|
export const getAllViews = (
|
||||||
sourcepage: DataSource,
|
sourcepage: DataSource | 'meter',
|
||||||
): Promise<AxiosResponse<AllViewsProps>> =>
|
): Promise<AxiosResponse<AllViewsProps>> =>
|
||||||
axios.get(`/explorer/views?sourcePage=${sourcepage}`);
|
axios.get(`/explorer/views?sourcePage=${sourcepage}`);
|
||||||
|
|||||||
@ -260,6 +260,7 @@ export function convertBuilderQueriesToV5(
|
|||||||
spec = {
|
spec = {
|
||||||
name: queryName,
|
name: queryName,
|
||||||
signal: 'metrics' as const,
|
signal: 'metrics' as const,
|
||||||
|
source: queryData.source || '',
|
||||||
...baseSpec,
|
...baseSpec,
|
||||||
aggregations: aggregations as MetricAggregation[],
|
aggregations: aggregations as MetricAggregation[],
|
||||||
// reduceTo: queryData.reduceTo,
|
// reduceTo: queryData.reduceTo,
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
.loading-panel-data {
|
||||||
|
padding: 24px 0;
|
||||||
|
height: 240px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.loading-panel-data-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.loading-gif {
|
||||||
|
height: 72px;
|
||||||
|
margin-left: -24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import './PanelDataLoading.styles.scss';
|
||||||
|
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
export function PanelDataLoading(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="loading-panel-data">
|
||||||
|
<div className="loading-panel-data-content">
|
||||||
|
<img
|
||||||
|
className="loading-gif"
|
||||||
|
src="/Icons/loading-plane.gif"
|
||||||
|
alt="wait-icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text>Fetching data...</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -131,6 +131,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
queryVariant={config?.queryVariant || 'dropdown'}
|
queryVariant={config?.queryVariant || 'dropdown'}
|
||||||
showOnlyWhereClause={showOnlyWhereClause}
|
showOnlyWhereClause={showOnlyWhereClause}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
|
signalSource={config?.signalSource || ''}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@ -18,11 +18,13 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
index,
|
index,
|
||||||
version,
|
version,
|
||||||
panelType,
|
panelType,
|
||||||
|
signalSource = '',
|
||||||
}: {
|
}: {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
index: number;
|
index: number;
|
||||||
version: string;
|
version: string;
|
||||||
panelType: PANEL_TYPES | null;
|
panelType: PANEL_TYPES | null;
|
||||||
|
signalSource: string;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { setAggregationOptions } = useQueryBuilderV2Context();
|
const { setAggregationOptions } = useQueryBuilderV2Context();
|
||||||
const {
|
const {
|
||||||
@ -208,6 +210,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
disabled={!queryAggregation.metricName}
|
disabled={!queryAggregation.metricName}
|
||||||
query={query}
|
query={query}
|
||||||
onChange={handleChangeGroupByKeys}
|
onChange={handleChangeGroupByKeys}
|
||||||
|
signalSource={signalSource}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -244,6 +247,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
disabled={!queryAggregation.metricName}
|
disabled={!queryAggregation.metricName}
|
||||||
query={query}
|
query={query}
|
||||||
onChange={handleChangeGroupByKeys}
|
onChange={handleChangeGroupByKeys}
|
||||||
|
signalSource={signalSource}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,10 +9,12 @@ export const MetricsSelect = memo(function MetricsSelect({
|
|||||||
query,
|
query,
|
||||||
index,
|
index,
|
||||||
version,
|
version,
|
||||||
|
signalSource,
|
||||||
}: {
|
}: {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
index: number;
|
index: number;
|
||||||
version: string;
|
version: string;
|
||||||
|
signalSource: 'meter' | '';
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { handleChangeAggregatorAttribute } = useQueryOperations({
|
const { handleChangeAggregatorAttribute } = useQueryOperations({
|
||||||
index,
|
index,
|
||||||
@ -26,6 +28,7 @@ export const MetricsSelect = memo(function MetricsSelect({
|
|||||||
onChange={handleChangeAggregatorAttribute}
|
onChange={handleChangeAggregatorAttribute}
|
||||||
query={query}
|
query={query}
|
||||||
index={index}
|
index={index}
|
||||||
|
signalSource={signalSource || ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -81,10 +81,12 @@ function QuerySearch({
|
|||||||
queryData,
|
queryData,
|
||||||
dataSource,
|
dataSource,
|
||||||
onRun,
|
onRun,
|
||||||
|
signalSource,
|
||||||
}: {
|
}: {
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
queryData: IBuilderQuery;
|
queryData: IBuilderQuery;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
signalSource?: string;
|
||||||
onRun?: (query: string) => void;
|
onRun?: (query: string) => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
@ -218,6 +220,7 @@ function QuerySearch({
|
|||||||
signal: dataSource,
|
signal: dataSource,
|
||||||
searchText: searchText || '',
|
searchText: searchText || '',
|
||||||
metricName: debouncedMetricName ?? undefined,
|
metricName: debouncedMetricName ?? undefined,
|
||||||
|
signalSource: signalSource as 'meter' | '',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.data) {
|
if (response.data.data) {
|
||||||
@ -245,6 +248,7 @@ function QuerySearch({
|
|||||||
keySuggestions,
|
keySuggestions,
|
||||||
toggleSuggestions,
|
toggleSuggestions,
|
||||||
queryData.aggregateAttribute?.key,
|
queryData.aggregateAttribute?.key,
|
||||||
|
signalSource,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -378,6 +382,8 @@ function QuerySearch({
|
|||||||
key,
|
key,
|
||||||
searchText: sanitizedSearchText,
|
searchText: sanitizedSearchText,
|
||||||
signal: dataSource,
|
signal: dataSource,
|
||||||
|
signalSource: signalSource as 'meter' | '',
|
||||||
|
metricName: debouncedMetricName ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Skip updates if component unmounted or key changed
|
// Skip updates if component unmounted or key changed
|
||||||
@ -465,8 +471,14 @@ function QuerySearch({
|
|||||||
setIsFetchingCompleteValuesList(false);
|
setIsFetchingCompleteValuesList(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
[
|
||||||
[activeKey, dataSource, isFocused],
|
activeKey,
|
||||||
|
dataSource,
|
||||||
|
isLoadingSuggestions,
|
||||||
|
debouncedMetricName,
|
||||||
|
signalSource,
|
||||||
|
toggleSuggestions,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedFetchValueSuggestions = useMemo(
|
const debouncedFetchValueSuggestions = useMemo(
|
||||||
@ -1440,6 +1452,7 @@ function QuerySearch({
|
|||||||
|
|
||||||
QuerySearch.defaultProps = {
|
QuerySearch.defaultProps = {
|
||||||
onRun: undefined,
|
onRun: undefined,
|
||||||
|
signalSource: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QuerySearch;
|
export default QuerySearch;
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
version,
|
version,
|
||||||
showOnlyWhereClause = false,
|
showOnlyWhereClause = false,
|
||||||
|
signalSource = '',
|
||||||
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
||||||
const { cloneQuery, panelType } = useQueryBuilder();
|
const { cloneQuery, panelType } = useQueryBuilder();
|
||||||
|
|
||||||
@ -175,6 +176,7 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
query={query}
|
query={query}
|
||||||
index={index}
|
index={index}
|
||||||
version={ENTITY_VERSION_V5}
|
version={ENTITY_VERSION_V5}
|
||||||
|
signalSource={signalSource as 'meter' | ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -186,6 +188,7 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
queryData={query}
|
queryData={query}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
|
signalSource={signalSource}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -218,6 +221,7 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
index={index}
|
index={index}
|
||||||
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
|
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
|
||||||
version="v4"
|
version="v4"
|
||||||
|
signalSource={signalSource as 'meter' | ''}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
|||||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
|
||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
|
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
|
||||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
@ -73,18 +74,59 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
searchText: searchText ?? '',
|
searchText: searchText ?? '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isOpen,
|
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: keyValueSuggestions,
|
||||||
|
isLoading: isLoadingKeyValueSuggestions,
|
||||||
|
} = useGetQueryKeyValueSuggestions({
|
||||||
|
key: filter.attributeKey.key,
|
||||||
|
signal: filter.dataSource || DataSource.LOGS,
|
||||||
|
signalSource: 'meter',
|
||||||
|
options: {
|
||||||
|
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const attributeValues: string[] = useMemo(() => {
|
const attributeValues: string[] = useMemo(() => {
|
||||||
const dataType = filter.attributeKey.dataType || DataTypes.String;
|
const dataType = filter.attributeKey.dataType || DataTypes.String;
|
||||||
|
|
||||||
|
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
|
||||||
|
// Process the response data
|
||||||
|
const responseData = keyValueSuggestions?.data as any;
|
||||||
|
const values = responseData.data?.values || {};
|
||||||
|
const stringValues = values.stringValues || [];
|
||||||
|
const numberValues = values.numberValues || [];
|
||||||
|
|
||||||
|
// Generate options from string values - explicitly handle empty strings
|
||||||
|
const stringOptions = stringValues
|
||||||
|
// Strict filtering for empty string - we'll handle it as a special case if needed
|
||||||
|
.filter(
|
||||||
|
(value: string | null | undefined): value is string =>
|
||||||
|
value !== null && value !== undefined && value !== '',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate options from number values
|
||||||
|
const numberOptions = numberValues
|
||||||
|
.filter(
|
||||||
|
(value: number | null | undefined): value is number =>
|
||||||
|
value !== null && value !== undefined,
|
||||||
|
)
|
||||||
|
.map((value: number) => value.toString());
|
||||||
|
|
||||||
|
// Combine all options and make sure we don't have duplicate labels
|
||||||
|
return [...stringOptions, ...numberOptions];
|
||||||
|
}
|
||||||
|
|
||||||
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
|
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
|
||||||
return (data?.payload?.[key] || []).filter(
|
return (data?.payload?.[key] || []).filter(
|
||||||
(val) => val !== undefined && val !== null,
|
(val) => val !== undefined && val !== null,
|
||||||
);
|
);
|
||||||
}, [data?.payload, filter.attributeKey.dataType]);
|
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
|
||||||
|
|
||||||
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
|
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
|
||||||
|
|
||||||
@ -478,12 +520,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
{isOpen && isLoading && !attributeValues.length && (
|
{isOpen &&
|
||||||
<section className="loading">
|
(isLoading || isLoadingKeyValueSuggestions) &&
|
||||||
<Skeleton paragraph={{ rows: 4 }} />
|
!attributeValues.length && (
|
||||||
</section>
|
<section className="loading">
|
||||||
)}
|
<Skeleton paragraph={{ rows: 4 }} />
|
||||||
{isOpen && !isLoading && (
|
</section>
|
||||||
|
)}
|
||||||
|
{isOpen && !isLoading && !isLoadingKeyValueSuggestions && (
|
||||||
<>
|
<>
|
||||||
{!isEmptyStateWithDocsEnabled && (
|
{!isEmptyStateWithDocsEnabled && (
|
||||||
<section className="search">
|
<section className="search">
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
.quick-filters-container {
|
.quick-filters-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.quick-filters-settings-container {
|
.quick-filters-settings-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -102,6 +104,37 @@
|
|||||||
margin: 8px 12px;
|
margin: 8px 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-filters-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.perilin-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background: radial-gradient(circle, #fff 10%, transparent 0);
|
||||||
|
background-size: 12px 12px;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
mask-image: radial-gradient(
|
||||||
|
circle at 50% 0,
|
||||||
|
rgba(11, 12, 14, 0.1) 0,
|
||||||
|
rgba(11, 12, 14, 0) 100%
|
||||||
|
);
|
||||||
|
-webkit-mask-image: radial-gradient(
|
||||||
|
circle at 50% 0,
|
||||||
|
rgba(11, 12, 14, 0.1) 0,
|
||||||
|
rgba(11, 12, 14, 0) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
|||||||
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
|
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { cloneDeep, isFunction, isNull } from 'lodash-es';
|
import { cloneDeep, isFunction, isNull } from 'lodash-es';
|
||||||
import { Settings2 as SettingsIcon } from 'lucide-react';
|
import { Frown, Settings2 as SettingsIcon } from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@ -236,6 +236,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{filterConfig.length === 0 && (
|
||||||
|
<div className="no-filters-container">
|
||||||
|
<Frown size={16} />
|
||||||
|
<Typography.Text>No filters found</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
</OverlayScrollbar>
|
</OverlayScrollbar>
|
||||||
|
|||||||
@ -6,4 +6,5 @@ export const SIGNAL_DATA_SOURCE_MAP = {
|
|||||||
[SignalType.TRACES]: DataSource.TRACES,
|
[SignalType.TRACES]: DataSource.TRACES,
|
||||||
[SignalType.EXCEPTIONS]: DataSource.TRACES,
|
[SignalType.EXCEPTIONS]: DataSource.TRACES,
|
||||||
[SignalType.API_MONITORING]: DataSource.TRACES,
|
[SignalType.API_MONITORING]: DataSource.TRACES,
|
||||||
|
[SignalType.METER_EXPLORER]: DataSource.METRICS,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -54,6 +54,7 @@ const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
|
|||||||
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
|
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
|
||||||
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
|
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
|
||||||
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
|
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
|
||||||
|
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
|
||||||
|
|
||||||
const FILTER_OS_DESCRIPTION = 'os.description';
|
const FILTER_OS_DESCRIPTION = 'os.description';
|
||||||
const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name';
|
const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name';
|
||||||
@ -77,7 +78,11 @@ const setupServer = (): void => {
|
|||||||
putHandler(await req.json());
|
putHandler(await req.json());
|
||||||
return res(ctx.status(200), ctx.json({}));
|
return res(ctx.status(200), ctx.json({}));
|
||||||
}),
|
}),
|
||||||
rest.get(quickFiltersAttributeValuesURL, (_, res, ctx) =>
|
|
||||||
|
rest.get(quickFiltersAttributeValuesURL, (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
||||||
|
),
|
||||||
|
rest.get(fieldsValuesURL, (req, res, ctx) =>
|
||||||
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export enum SignalType {
|
|||||||
LOGS = 'logs',
|
LOGS = 'logs',
|
||||||
API_MONITORING = 'api_monitoring',
|
API_MONITORING = 'api_monitoring',
|
||||||
EXCEPTIONS = 'exceptions',
|
EXCEPTIONS = 'exceptions',
|
||||||
|
METER_EXPLORER = 'meter',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IQuickFiltersConfig {
|
export interface IQuickFiltersConfig {
|
||||||
@ -53,4 +54,5 @@ export enum QuickFiltersSource {
|
|||||||
TRACES_EXPLORER = 'traces-explorer',
|
TRACES_EXPLORER = 'traces-explorer',
|
||||||
API_MONITORING = 'api-monitoring',
|
API_MONITORING = 'api-monitoring',
|
||||||
EXCEPTIONS = 'exceptions',
|
EXCEPTIONS = 'exceptions',
|
||||||
|
METER_EXPLORER = 'meter',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
BoolOperators,
|
BoolOperators,
|
||||||
DataSource,
|
DataSource,
|
||||||
LogsAggregatorOperator,
|
LogsAggregatorOperator,
|
||||||
|
MeterAggregateOperator,
|
||||||
MetricAggregateOperator,
|
MetricAggregateOperator,
|
||||||
NumberOperators,
|
NumberOperators,
|
||||||
QueryAdditionalFilter,
|
QueryAdditionalFilter,
|
||||||
@ -36,6 +37,7 @@ import { v4 as uuid } from 'uuid';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
logsAggregateOperatorOptions,
|
logsAggregateOperatorOptions,
|
||||||
|
meterAggregateOperatorOptions,
|
||||||
metricAggregateOperatorOptions,
|
metricAggregateOperatorOptions,
|
||||||
metricsGaugeAggregateOperatorOptions,
|
metricsGaugeAggregateOperatorOptions,
|
||||||
metricsGaugeSpaceAggregateOperatorOptions,
|
metricsGaugeSpaceAggregateOperatorOptions,
|
||||||
@ -79,6 +81,7 @@ export const mapOfOperators = {
|
|||||||
metrics: metricAggregateOperatorOptions,
|
metrics: metricAggregateOperatorOptions,
|
||||||
logs: logsAggregateOperatorOptions,
|
logs: logsAggregateOperatorOptions,
|
||||||
traces: tracesAggregateOperatorOptions,
|
traces: tracesAggregateOperatorOptions,
|
||||||
|
meter: meterAggregateOperatorOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const metricsOperatorsByType = {
|
export const metricsOperatorsByType = {
|
||||||
@ -193,6 +196,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
groupBy: [],
|
groupBy: [],
|
||||||
legend: '',
|
legend: '',
|
||||||
reduceTo: 'avg',
|
reduceTo: 'avg',
|
||||||
|
source: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
|
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
|
||||||
@ -209,6 +213,39 @@ const initialQueryBuilderFormTracesValues: IBuilderQuery = {
|
|||||||
dataSource: DataSource.TRACES,
|
dataSource: DataSource.TRACES,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initialQueryBuilderFormMeterValues: IBuilderQuery = {
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||||
|
aggregateOperator: MeterAggregateOperator.COUNT,
|
||||||
|
aggregateAttribute: initialAutocompleteData,
|
||||||
|
timeAggregation: MeterAggregateOperator.RATE,
|
||||||
|
spaceAggregation: MeterAggregateOperator.SUM,
|
||||||
|
filter: { expression: '' },
|
||||||
|
aggregations: [
|
||||||
|
{
|
||||||
|
metricName: '',
|
||||||
|
temporality: '',
|
||||||
|
timeAggregation: MeterAggregateOperator.COUNT,
|
||||||
|
spaceAggregation: MeterAggregateOperator.SUM,
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
functions: [],
|
||||||
|
filters: { items: [], op: 'AND' },
|
||||||
|
expression: createNewBuilderItemName({
|
||||||
|
existNames: [],
|
||||||
|
sourceNames: alphabet,
|
||||||
|
}),
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: undefined,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
};
|
||||||
|
|
||||||
export const initialQueryBuilderFormValuesMap: Record<
|
export const initialQueryBuilderFormValuesMap: Record<
|
||||||
DataSource,
|
DataSource,
|
||||||
IBuilderQuery
|
IBuilderQuery
|
||||||
@ -285,6 +322,19 @@ export const initialQueriesMap: Record<DataSource, Query> = {
|
|||||||
traces: initialQueryTracesWithType,
|
traces: initialQueryTracesWithType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initialQueryMeterWithType: Query = {
|
||||||
|
...initialQueryWithType,
|
||||||
|
builder: {
|
||||||
|
...initialQueryWithType.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueryBuilderFormValuesMap.metrics,
|
||||||
|
source: 'meter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
||||||
string: Object.values(StringOperators),
|
string: Object.values(StringOperators),
|
||||||
number: Object.values(NumberOperators),
|
number: Object.values(NumberOperators),
|
||||||
|
|||||||
@ -125,6 +125,126 @@ export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const meterAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.COUNT,
|
||||||
|
label: 'Count',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.COUNT_DISTINCT,
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
label: 'Count Distinct',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.SUM,
|
||||||
|
label: 'Sum',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.AVG,
|
||||||
|
label: 'Avg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.MAX,
|
||||||
|
label: 'Max',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.MIN,
|
||||||
|
label: 'Min',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.P05,
|
||||||
|
label: 'P05',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.P10,
|
||||||
|
label: 'P10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.P20,
|
||||||
|
label: 'P20',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.P25,
|
||||||
|
label: 'P25',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.P50,
|
||||||
|
label: 'P50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.P75,
|
||||||
|
label: 'P75',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.P90,
|
||||||
|
label: 'P90',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.P95,
|
||||||
|
label: 'P95',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.P99,
|
||||||
|
label: 'P99',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.RATE,
|
||||||
|
label: 'Rate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.SUM_RATE,
|
||||||
|
label: 'Sum_rate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.AVG_RATE,
|
||||||
|
label: 'Avg_rate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.MAX_RATE,
|
||||||
|
label: 'Max_rate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.MIN_RATE,
|
||||||
|
label: 'Min_rate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.RATE_SUM,
|
||||||
|
label: 'Rate_sum',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.RATE_AVG,
|
||||||
|
label: 'Rate_avg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.RATE_MIN,
|
||||||
|
label: 'Rate_min',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.RATE_MAX,
|
||||||
|
label: 'Rate_max',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.HIST_QUANTILE_50,
|
||||||
|
label: 'Hist_quantile_50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.HIST_QUANTILE_75,
|
||||||
|
label: 'Hist_quantile_75',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.HIST_QUANTILE_90,
|
||||||
|
label: 'Hist_quantile_90',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.HIST_QUANTILE_95,
|
||||||
|
label: 'Hist_quantile_95',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MetricAggregateOperator.HIST_QUANTILE_99,
|
||||||
|
label: 'Hist_quantile_99',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||||
{
|
{
|
||||||
value: TracesAggregatorOperator.COUNT,
|
value: TracesAggregatorOperator.COUNT,
|
||||||
|
|||||||
@ -77,6 +77,9 @@ const ROUTES = {
|
|||||||
API_MONITORING: '/api-monitoring/explorer',
|
API_MONITORING: '/api-monitoring/explorer',
|
||||||
METRICS_EXPLORER_BASE: '/metrics-explorer',
|
METRICS_EXPLORER_BASE: '/metrics-explorer',
|
||||||
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
||||||
|
METER_EXPLORER_BASE: '/meter-explorer',
|
||||||
|
METER_EXPLORER: '/meter-explorer',
|
||||||
|
METER_EXPLORER_VIEWS: '/meter-explorer/views',
|
||||||
HOME_PAGE: '/',
|
HOME_PAGE: '/',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export const GlobalShortcuts = {
|
|||||||
NavigateToAlerts: 'a+shift',
|
NavigateToAlerts: 'a+shift',
|
||||||
NavigateToExceptions: 'e+shift',
|
NavigateToExceptions: 'e+shift',
|
||||||
NavigateToMessagingQueues: 'm+shift',
|
NavigateToMessagingQueues: 'm+shift',
|
||||||
|
ToggleSidebar: 'b+shift',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GlobalShortcutsName = {
|
export const GlobalShortcutsName = {
|
||||||
@ -16,6 +17,7 @@ export const GlobalShortcutsName = {
|
|||||||
NavigateToAlerts: 'shift+a',
|
NavigateToAlerts: 'shift+a',
|
||||||
NavigateToExceptions: 'shift+e',
|
NavigateToExceptions: 'shift+e',
|
||||||
NavigateToMessagingQueues: 'shift+m',
|
NavigateToMessagingQueues: 'shift+m',
|
||||||
|
ToggleSidebar: 'shift+b',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GlobalShortcutsDescription = {
|
export const GlobalShortcutsDescription = {
|
||||||
@ -26,4 +28,5 @@ export const GlobalShortcutsDescription = {
|
|||||||
NavigateToAlerts: 'Navigate to alerts page',
|
NavigateToAlerts: 'Navigate to alerts page',
|
||||||
NavigateToExceptions: 'Navigate to Exceptions page',
|
NavigateToExceptions: 'Navigate to Exceptions page',
|
||||||
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
|
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
|
||||||
|
ToggleSidebar: 'Toggle sidebar visibility',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,176 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
|
||||||
|
import { USER_PREFERENCES } from 'constants/userPreferences';
|
||||||
|
import {
|
||||||
|
KeyboardHotkeysProvider,
|
||||||
|
useKeyboardHotkeys,
|
||||||
|
} from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('api/common/logEvent', () => jest.fn());
|
||||||
|
|
||||||
|
// Mock the AppContext
|
||||||
|
const mockUpdateUserPreferenceInContext = jest.fn();
|
||||||
|
|
||||||
|
const SHIFT_B_KEYBOARD_SHORTCUT = '{Shift>}b{/Shift}';
|
||||||
|
|
||||||
|
jest.mock('providers/App/App', () => ({
|
||||||
|
useAppContext: jest.fn(() => ({
|
||||||
|
userPreferences: [
|
||||||
|
{
|
||||||
|
name: USER_PREFERENCES.SIDENAV_PINNED,
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
updateUserPreferenceInContext: mockUpdateUserPreferenceInContext,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function TestComponent({
|
||||||
|
mockHandleShortcut,
|
||||||
|
}: {
|
||||||
|
mockHandleShortcut: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { registerShortcut } = useKeyboardHotkeys();
|
||||||
|
registerShortcut(GlobalShortcuts.ToggleSidebar, mockHandleShortcut);
|
||||||
|
return <div data-testid="test">Test</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Sidebar Toggle Shortcut', () => {
|
||||||
|
let queryClient: QueryClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Global Shortcuts Constants', () => {
|
||||||
|
it('should have the correct shortcut key combination', () => {
|
||||||
|
expect(GlobalShortcuts.ToggleSidebar).toBe('b+shift');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Keyboard Shortcut Registration', () => {
|
||||||
|
it('should register the sidebar toggle shortcut correctly', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const mockHandleShortcut = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<KeyboardHotkeysProvider>
|
||||||
|
<TestComponent mockHandleShortcut={mockHandleShortcut} />
|
||||||
|
</KeyboardHotkeysProvider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger the shortcut
|
||||||
|
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
|
||||||
|
|
||||||
|
expect(mockHandleShortcut).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not trigger shortcut in input fields', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const mockHandleShortcut = jest.fn();
|
||||||
|
|
||||||
|
function TestComponent(): JSX.Element {
|
||||||
|
const { registerShortcut } = useKeyboardHotkeys();
|
||||||
|
registerShortcut(GlobalShortcuts.ToggleSidebar, mockHandleShortcut);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input data-testid="input-field" />
|
||||||
|
<div data-testid="test">Test</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<KeyboardHotkeysProvider>
|
||||||
|
<TestComponent />
|
||||||
|
</KeyboardHotkeysProvider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Focus on input field
|
||||||
|
const inputField = screen.getByTestId('input-field');
|
||||||
|
await user.click(inputField);
|
||||||
|
|
||||||
|
// Try to trigger shortcut while focused on input
|
||||||
|
await user.keyboard('{Shift>}b{/Shift}');
|
||||||
|
|
||||||
|
// Should not trigger the shortcut
|
||||||
|
expect(mockHandleShortcut).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Sidebar Toggle Functionality', () => {
|
||||||
|
it('should log the toggle event with correct parameters', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const mockHandleShortcut = jest.fn(() => {
|
||||||
|
logEvent('Global Shortcut: Sidebar Toggle', {
|
||||||
|
previousState: false,
|
||||||
|
newState: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<KeyboardHotkeysProvider>
|
||||||
|
<TestComponent mockHandleShortcut={mockHandleShortcut} />
|
||||||
|
</KeyboardHotkeysProvider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
|
||||||
|
|
||||||
|
expect(logEvent).toHaveBeenCalledWith('Global Shortcut: Sidebar Toggle', {
|
||||||
|
previousState: false,
|
||||||
|
newState: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update user preference in context', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const mockHandleShortcut = jest.fn(() => {
|
||||||
|
const save = {
|
||||||
|
name: USER_PREFERENCES.SIDENAV_PINNED,
|
||||||
|
value: true,
|
||||||
|
};
|
||||||
|
mockUpdateUserPreferenceInContext(save);
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<KeyboardHotkeysProvider>
|
||||||
|
<TestComponent mockHandleShortcut={mockHandleShortcut} />
|
||||||
|
</KeyboardHotkeysProvider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
|
||||||
|
|
||||||
|
expect(mockUpdateUserPreferenceInContext).toHaveBeenCalledWith({
|
||||||
|
name: USER_PREFERENCES.SIDENAV_PINNED,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -10,8 +10,10 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
|
|||||||
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
|
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import manageCreditCardApi from 'api/v1/portal/create';
|
import manageCreditCardApi from 'api/v1/portal/create';
|
||||||
|
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||||
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
||||||
import getUserVersion from 'api/v1/version/getVersion';
|
import getUserVersion from 'api/v1/version/getVersion';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
|
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
|
||||||
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||||
@ -22,10 +24,12 @@ import { Events } from 'constants/events';
|
|||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
|
||||||
import { USER_PREFERENCES } from 'constants/userPreferences';
|
import { USER_PREFERENCES } from 'constants/userPreferences';
|
||||||
import SideNav from 'container/SideNav';
|
import SideNav from 'container/SideNav';
|
||||||
import TopNav from 'container/TopNav';
|
import TopNav from 'container/TopNav';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@ -68,8 +72,10 @@ import {
|
|||||||
LicensePlatform,
|
LicensePlatform,
|
||||||
LicenseState,
|
LicenseState,
|
||||||
} from 'types/api/licensesV3/getActive';
|
} from 'types/api/licensesV3/getActive';
|
||||||
|
import { UserPreference } from 'types/api/preferences/preference';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
import { showErrorNotification } from 'utils/error';
|
||||||
import { eventEmitter } from 'utils/getEventEmitter';
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
import {
|
import {
|
||||||
getFormattedDate,
|
getFormattedDate,
|
||||||
@ -662,10 +668,85 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const sideNavPinned = userPreferences?.find(
|
const sideNavPinnedPreference = userPreferences?.find(
|
||||||
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
|
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
|
||||||
)?.value as boolean;
|
)?.value as boolean;
|
||||||
|
|
||||||
|
// Add loading state to prevent layout shift during initial load
|
||||||
|
const [isSidebarLoaded, setIsSidebarLoaded] = useState(false);
|
||||||
|
|
||||||
|
// Get sidebar state from localStorage as fallback until preferences are loaded
|
||||||
|
const getSidebarStateFromLocalStorage = useCallback((): boolean => {
|
||||||
|
try {
|
||||||
|
const storedValue = getLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED);
|
||||||
|
return storedValue === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set sidebar as loaded after user preferences are fetched
|
||||||
|
useEffect(() => {
|
||||||
|
if (userPreferences !== null) {
|
||||||
|
setIsSidebarLoaded(true);
|
||||||
|
}
|
||||||
|
}, [userPreferences]);
|
||||||
|
|
||||||
|
// Use localStorage value as fallback until preferences are loaded
|
||||||
|
const isSideNavPinned = isSidebarLoaded
|
||||||
|
? sideNavPinnedPreference
|
||||||
|
: getSidebarStateFromLocalStorage();
|
||||||
|
|
||||||
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||||
|
const { updateUserPreferenceInContext } = useAppContext();
|
||||||
|
|
||||||
|
const { mutate: updateUserPreferenceMutation } = useMutation(
|
||||||
|
updateUserPreference,
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
showErrorNotification(notifications, error as AxiosError);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleSidebar = useCallback((): void => {
|
||||||
|
const newState = !isSideNavPinned;
|
||||||
|
|
||||||
|
logEvent('Global Shortcut: Sidebar Toggle', {
|
||||||
|
previousState: isSideNavPinned,
|
||||||
|
newState,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to localStorage immediately for instant feedback
|
||||||
|
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, newState.toString());
|
||||||
|
|
||||||
|
// Update the context immediately
|
||||||
|
const save = {
|
||||||
|
name: USER_PREFERENCES.SIDENAV_PINNED,
|
||||||
|
value: newState,
|
||||||
|
};
|
||||||
|
updateUserPreferenceInContext(save as UserPreference);
|
||||||
|
|
||||||
|
// Make the API call in the background
|
||||||
|
updateUserPreferenceMutation({
|
||||||
|
name: USER_PREFERENCES.SIDENAV_PINNED,
|
||||||
|
value: newState,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
isSideNavPinned,
|
||||||
|
updateUserPreferenceInContext,
|
||||||
|
updateUserPreferenceMutation,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Register the sidebar toggle shortcut
|
||||||
|
useEffect(() => {
|
||||||
|
registerShortcut(GlobalShortcuts.ToggleSidebar, handleToggleSidebar);
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
deregisterShortcut(GlobalShortcuts.ToggleSidebar);
|
||||||
|
};
|
||||||
|
}, [registerShortcut, deregisterShortcut, handleToggleSidebar]);
|
||||||
|
|
||||||
const SHOW_TRIAL_EXPIRY_BANNER =
|
const SHOW_TRIAL_EXPIRY_BANNER =
|
||||||
showTrialExpiryBanner && !showPaymentFailedWarning;
|
showTrialExpiryBanner && !showPaymentFailedWarning;
|
||||||
const SHOW_WORKSPACE_RESTRICTED_BANNER = showWorkspaceRestricted;
|
const SHOW_WORKSPACE_RESTRICTED_BANNER = showWorkspaceRestricted;
|
||||||
@ -739,14 +820,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
className={cx(
|
className={cx(
|
||||||
'app-layout',
|
'app-layout',
|
||||||
isDarkMode ? 'darkMode dark' : 'lightMode',
|
isDarkMode ? 'darkMode dark' : 'lightMode',
|
||||||
sideNavPinned ? 'side-nav-pinned' : '',
|
isSideNavPinned ? 'side-nav-pinned' : '',
|
||||||
SHOW_WORKSPACE_RESTRICTED_BANNER ? 'isWorkspaceRestricted' : '',
|
SHOW_WORKSPACE_RESTRICTED_BANNER ? 'isWorkspaceRestricted' : '',
|
||||||
SHOW_TRIAL_EXPIRY_BANNER ? 'isTrialExpired' : '',
|
SHOW_TRIAL_EXPIRY_BANNER ? 'isTrialExpired' : '',
|
||||||
SHOW_PAYMENT_FAILED_BANNER ? 'isPaymentFailed' : '',
|
SHOW_PAYMENT_FAILED_BANNER ? 'isPaymentFailed' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isToDisplayLayout && !renderFullScreen && (
|
{isToDisplayLayout && !renderFullScreen && (
|
||||||
<SideNav isPinned={sideNavPinned} />
|
<SideNav isPinned={isSideNavPinned} />
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={cx('app-content', {
|
className={cx('app-content', {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ function ExplorerOptionWrapper({
|
|||||||
sourcepage,
|
sourcepage,
|
||||||
isOneChartPerQuery,
|
isOneChartPerQuery,
|
||||||
splitedQueries,
|
splitedQueries,
|
||||||
|
signalSource,
|
||||||
}: ExplorerOptionsWrapperProps): JSX.Element {
|
}: ExplorerOptionsWrapperProps): JSX.Element {
|
||||||
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
|
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ function ExplorerOptionWrapper({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onExport={onExport}
|
onExport={onExport}
|
||||||
sourcepage={sourcepage}
|
sourcepage={sourcepage}
|
||||||
|
signalSource={signalSource}
|
||||||
isExplorerOptionHidden={isExplorerOptionHidden}
|
isExplorerOptionHidden={isExplorerOptionHidden}
|
||||||
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
||||||
isOneChartPerQuery={isOneChartPerQuery}
|
isOneChartPerQuery={isOneChartPerQuery}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
.explorer-options-container {
|
.explorer-options-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px;
|
bottom: 8px;
|
||||||
left: calc(50% + 240px);
|
left: calc(50% + 240px);
|
||||||
transform: translate(calc(-50% - 120px), 0);
|
transform: translate(calc(-50% - 120px), 0);
|
||||||
transition: left 0.2s linear;
|
transition: left 0.2s linear;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
.multi-alert-button,
|
.multi-alert-button,
|
||||||
@ -33,11 +33,12 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 10px 10px;
|
padding: 10px 12px;
|
||||||
border-radius: 50px;
|
|
||||||
border: 1px solid var(--bg-slate-400);
|
|
||||||
background: rgba(22, 24, 29, 0.6);
|
background: rgba(22, 24, 29, 0.6);
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
border-radius: 4px;
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
.action-icon {
|
.action-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -64,9 +65,9 @@
|
|||||||
|
|
||||||
.explorer-options {
|
.explorer-options {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid var(--bg-slate-400);
|
border-radius: 4px;
|
||||||
border-radius: 50px;
|
border: 1px solid var(--bg-slate-500);
|
||||||
background: rgba(22, 24, 29, 0.6);
|
background: var(--bg-ink-400);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|||||||
@ -93,6 +93,7 @@ function ExplorerOptions({
|
|||||||
onExport,
|
onExport,
|
||||||
query,
|
query,
|
||||||
sourcepage,
|
sourcepage,
|
||||||
|
signalSource,
|
||||||
isExplorerOptionHidden = false,
|
isExplorerOptionHidden = false,
|
||||||
setIsExplorerOptionHidden,
|
setIsExplorerOptionHidden,
|
||||||
isOneChartPerQuery = false,
|
isOneChartPerQuery = false,
|
||||||
@ -110,6 +111,7 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
const isLogsExplorer = sourcepage === DataSource.LOGS;
|
const isLogsExplorer = sourcepage === DataSource.LOGS;
|
||||||
const isMetricsExplorer = sourcepage === DataSource.METRICS;
|
const isMetricsExplorer = sourcepage === DataSource.METRICS;
|
||||||
|
const isMeterExplorer = signalSource === 'meter';
|
||||||
|
|
||||||
const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS;
|
const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS;
|
||||||
|
|
||||||
@ -120,8 +122,11 @@ function ExplorerOptions({
|
|||||||
if (isMetricsExplorer) {
|
if (isMetricsExplorer) {
|
||||||
return PreservedViewsTypes.METRICS;
|
return PreservedViewsTypes.METRICS;
|
||||||
}
|
}
|
||||||
|
if (isMeterExplorer) {
|
||||||
|
return PreservedViewsTypes.METER;
|
||||||
|
}
|
||||||
return PreservedViewsTypes.TRACES;
|
return PreservedViewsTypes.TRACES;
|
||||||
}, [isLogsExplorer, isMetricsExplorer]);
|
}, [isLogsExplorer, isMetricsExplorer, isMeterExplorer]);
|
||||||
|
|
||||||
const onModalToggle = useCallback((value: boolean) => {
|
const onModalToggle = useCallback((value: boolean) => {
|
||||||
setIsExport(value);
|
setIsExport(value);
|
||||||
@ -150,6 +155,10 @@ function ExplorerOptions({
|
|||||||
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
|
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
|
||||||
panelType,
|
panelType,
|
||||||
});
|
});
|
||||||
|
} else if (isMeterExplorer) {
|
||||||
|
logEvent('Meter Explorer: Save view clicked', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setIsSaveModalOpen(!isSaveModalOpen);
|
setIsSaveModalOpen(!isSaveModalOpen);
|
||||||
};
|
};
|
||||||
@ -243,7 +252,7 @@ function ExplorerOptions({
|
|||||||
error,
|
error,
|
||||||
isRefetching,
|
isRefetching,
|
||||||
refetch: refetchAllView,
|
refetch: refetchAllView,
|
||||||
} = useGetAllViews(sourcepage);
|
} = useGetAllViews(isMeterExplorer ? 'meter' : sourcepage);
|
||||||
|
|
||||||
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||||
|
|
||||||
@ -316,7 +325,7 @@ function ExplorerOptions({
|
|||||||
compositeQuery,
|
compositeQuery,
|
||||||
viewKey,
|
viewKey,
|
||||||
extraData: updatedExtraData,
|
extraData: updatedExtraData,
|
||||||
sourcePage: sourcepage,
|
sourcePage: isMeterExplorer ? 'meter' : sourcepage,
|
||||||
viewName,
|
viewName,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -332,7 +341,7 @@ function ExplorerOptions({
|
|||||||
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
|
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
|
||||||
viewKey,
|
viewKey,
|
||||||
extraData: updatedExtraData,
|
extraData: updatedExtraData,
|
||||||
sourcePage: sourcepage,
|
sourcePage: isMeterExplorer ? 'meter' : sourcepage,
|
||||||
viewName,
|
viewName,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -459,6 +468,11 @@ function ExplorerOptions({
|
|||||||
panelType,
|
panelType,
|
||||||
viewName: option?.value,
|
viewName: option?.value,
|
||||||
});
|
});
|
||||||
|
} else if (isMeterExplorer) {
|
||||||
|
logEvent('Meter Explorer: Select view', {
|
||||||
|
panelType,
|
||||||
|
viewName: option?.value,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePreservedViewInLocalStorage(option);
|
updatePreservedViewInLocalStorage(option);
|
||||||
@ -505,6 +519,11 @@ function ExplorerOptions({
|
|||||||
: defaultLogsSelectedColumns,
|
: defaultLogsSelectedColumns,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (signalSource === 'meter') {
|
||||||
|
history.replace(ROUTES.METER_EXPLORER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
|
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -549,7 +568,7 @@ function ExplorerOptions({
|
|||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
refetchAllView,
|
refetchAllView,
|
||||||
saveViewAsync,
|
saveViewAsync,
|
||||||
sourcePage: sourcepage,
|
sourcePage: isMeterExplorer ? 'meter' : sourcepage,
|
||||||
viewName: newViewName,
|
viewName: newViewName,
|
||||||
setNewViewName,
|
setNewViewName,
|
||||||
});
|
});
|
||||||
@ -668,7 +687,7 @@ function ExplorerOptions({
|
|||||||
return `Query ${query.builder.queryData[0].queryName}`;
|
return `Query ${query.builder.queryData[0].queryName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const alertButton = useMemo(() => {
|
const CreateAlertButton = useMemo(() => {
|
||||||
if (isOneChartPerQuery) {
|
if (isOneChartPerQuery) {
|
||||||
const selectLabel = (
|
const selectLabel = (
|
||||||
<Button
|
<Button
|
||||||
@ -721,7 +740,7 @@ function ExplorerOptions({
|
|||||||
splitedQueries,
|
splitedQueries,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const dashboardButton = useMemo(() => {
|
const AddToDashboardButton = useMemo(() => {
|
||||||
if (isOneChartPerQuery) {
|
if (isOneChartPerQuery) {
|
||||||
const selectLabel = (
|
const selectLabel = (
|
||||||
<Button
|
<Button
|
||||||
@ -829,7 +848,7 @@ function ExplorerOptions({
|
|||||||
style={{
|
style={{
|
||||||
background: extraData
|
background: extraData
|
||||||
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
||||||
: 'transparent',
|
: 'initial',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="view-options">
|
<div className="view-options">
|
||||||
@ -884,10 +903,13 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
||||||
|
|
||||||
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
{signalSource !== 'meter' && (
|
||||||
{alertButton}
|
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
||||||
{dashboardButton}
|
{CreateAlertButton}
|
||||||
</div>
|
{AddToDashboardButton}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{/* Hide the info icon for metrics explorer until we get the docs link */}
|
{/* Hide the info icon for metrics explorer until we get the docs link */}
|
||||||
{!isMetricsExplorer && (
|
{!isMetricsExplorer && (
|
||||||
@ -993,6 +1015,7 @@ export interface ExplorerOptionsProps {
|
|||||||
query: Query | null;
|
query: Query | null;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
sourcepage: DataSource;
|
sourcepage: DataSource;
|
||||||
|
signalSource?: string;
|
||||||
isExplorerOptionHidden?: boolean;
|
isExplorerOptionHidden?: boolean;
|
||||||
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
|
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
|
||||||
isOneChartPerQuery?: boolean;
|
isOneChartPerQuery?: boolean;
|
||||||
@ -1005,6 +1028,7 @@ ExplorerOptions.defaultProps = {
|
|||||||
setIsExplorerOptionHidden: undefined,
|
setIsExplorerOptionHidden: undefined,
|
||||||
isOneChartPerQuery: false,
|
isOneChartPerQuery: false,
|
||||||
splitedQueries: [],
|
splitedQueries: [],
|
||||||
|
signalSource: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExplorerOptions;
|
export default ExplorerOptions;
|
||||||
|
|||||||
@ -2,4 +2,5 @@ export enum PreservedViewsTypes {
|
|||||||
LOGS = 'logs',
|
LOGS = 'logs',
|
||||||
TRACES = 'traces',
|
TRACES = 'traces',
|
||||||
METRICS = 'metrics',
|
METRICS = 'metrics',
|
||||||
|
METER = 'meter',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { PreservedViewsTypes } from './constants';
|
|||||||
export interface SaveNewViewHandlerProps {
|
export interface SaveNewViewHandlerProps {
|
||||||
viewName: string;
|
viewName: string;
|
||||||
compositeQuery: ICompositeMetricQuery;
|
compositeQuery: ICompositeMetricQuery;
|
||||||
sourcePage: DataSource;
|
sourcePage: DataSource | 'meter';
|
||||||
extraData: SaveViewProps['extraData'];
|
extraData: SaveViewProps['extraData'];
|
||||||
panelType: PANEL_TYPES | null;
|
panelType: PANEL_TYPES | null;
|
||||||
notifications: NotificationInstance;
|
notifications: NotificationInstance;
|
||||||
@ -32,7 +32,8 @@ export interface SaveNewViewHandlerProps {
|
|||||||
export type PreservedViewType =
|
export type PreservedViewType =
|
||||||
| PreservedViewsTypes.LOGS
|
| PreservedViewsTypes.LOGS
|
||||||
| PreservedViewsTypes.TRACES
|
| PreservedViewsTypes.TRACES
|
||||||
| PreservedViewsTypes.METRICS;
|
| PreservedViewsTypes.METRICS
|
||||||
|
| PreservedViewsTypes.METER;
|
||||||
|
|
||||||
export type PreservedViewsInLocalStorage = Partial<
|
export type PreservedViewsInLocalStorage = Partial<
|
||||||
Record<PreservedViewType, { key: string; value: string }>
|
Record<PreservedViewType, { key: string; value: string }>
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export const saveNewViewHandler = ({
|
|||||||
{
|
{
|
||||||
viewName,
|
viewName,
|
||||||
compositeQuery,
|
compositeQuery,
|
||||||
sourcePage,
|
sourcePage: sourcePage as DataSource,
|
||||||
extraData,
|
extraData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -73,6 +73,8 @@ export default function TableRow({
|
|||||||
{tableColumns.map((column) => {
|
{tableColumns.map((column) => {
|
||||||
if (!column.render) return <td>Empty</td>;
|
if (!column.render) return <td>Empty</td>;
|
||||||
|
|
||||||
|
if (!column.key) return null;
|
||||||
|
|
||||||
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
|
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
|
||||||
log[column.key as keyof Record<string, unknown>],
|
log[column.key as keyof Record<string, unknown>],
|
||||||
log,
|
log,
|
||||||
@ -97,6 +99,7 @@ export default function TableRow({
|
|||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
columnKey={column.key as string}
|
columnKey={column.key as string}
|
||||||
onClick={handleShowLogDetails}
|
onClick={handleShowLogDetails}
|
||||||
|
className={column.key as string}
|
||||||
>
|
>
|
||||||
{cloneElement(children, props)}
|
{cloneElement(children, props)}
|
||||||
</TableCellStyled>
|
</TableCellStyled>
|
||||||
|
|||||||
@ -136,7 +136,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
key={column.key}
|
key={column.key}
|
||||||
fontSize={tableViewProps?.fontSize}
|
fontSize={tableViewProps?.fontSize}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...(isDragColumn && { className: 'dragHandler' })}
|
{...(isDragColumn && { className: `dragHandler ${column.key}` })}
|
||||||
columnKey={column.key as string}
|
columnKey={column.key as string}
|
||||||
>
|
>
|
||||||
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}
|
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
import {
|
import {
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
@ -8,11 +9,13 @@ import {
|
|||||||
export const convertKeysToColumnFields = (
|
export const convertKeysToColumnFields = (
|
||||||
keys: TelemetryFieldKey[],
|
keys: TelemetryFieldKey[],
|
||||||
): IField[] =>
|
): IField[] =>
|
||||||
keys.map((item) => ({
|
keys
|
||||||
dataType: item.fieldDataType ?? '',
|
.filter((item) => !isEmpty(item.name))
|
||||||
name: item.name,
|
.map((item) => ({
|
||||||
type: item.fieldContext ?? '',
|
dataType: item.fieldDataType ?? '',
|
||||||
}));
|
name: item.name,
|
||||||
|
type: item.fieldContext ?? '',
|
||||||
|
}));
|
||||||
/**
|
/**
|
||||||
* Determines if a query represents a trace-to-logs navigation
|
* Determines if a query represents a trace-to-logs navigation
|
||||||
* by checking for the presence of a trace_id filter.
|
* by checking for the presence of a trace_id filter.
|
||||||
|
|||||||
@ -0,0 +1,195 @@
|
|||||||
|
.meter-explorer-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.meter-explorer-quick-filters-section {
|
||||||
|
width: 280px;
|
||||||
|
border-right: 1px solid var(--bg-slate-500);
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter-explorer-content-section {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.explore-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 4px 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
.explore-header-left-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explore-header-right-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-section {
|
||||||
|
max-height: 450px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.rc-virtual-list-holder {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.explore-tabs {
|
||||||
|
margin: 15px 0;
|
||||||
|
.tab {
|
||||||
|
background-color: var(--bg-slate-500);
|
||||||
|
border-color: var(--bg-ink-200);
|
||||||
|
width: 180px;
|
||||||
|
padding: 16px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:first-of-type {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:last-of-type {
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-view {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.explore-content {
|
||||||
|
.ant-space {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-meter-search {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-series-view-panel {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
padding: 8px !important;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-series-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(
|
||||||
|
auto-fit,
|
||||||
|
minmax(min(100%, calc(50% - 8px)), 1fr)
|
||||||
|
);
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.quick-filters-open {
|
||||||
|
.meter-explorer-content-section {
|
||||||
|
width: calc(100% - 280px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter-time-series-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.builder-units-filter {
|
||||||
|
padding: 0 8px;
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
|
||||||
|
.builder-units-filter-label {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.meter-explorer-container {
|
||||||
|
.explore-tabs {
|
||||||
|
.tab {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-view {
|
||||||
|
background: var(--bg-vanilla-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboards-and-alerts-popover-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.dashboards-and-alerts-popover {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboards-popover {
|
||||||
|
border: 1px solid var(--bg-sienna-500);
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-sienna-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-popover {
|
||||||
|
border: 1px solid var(--bg-sakura-500);
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-sakura-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.no-data-text {
|
||||||
|
color: var(--text-vanilla-500);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
182
frontend/src/container/MeterExplorer/Explorer/Explorer.tsx
Normal file
182
frontend/src/container/MeterExplorer/Explorer/Explorer.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import './Explorer.styles.scss';
|
||||||
|
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
|
import { Button, Tooltip } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||||
|
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
|
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||||
|
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||||
|
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||||
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
|
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
|
import { Filter } from 'lucide-react';
|
||||||
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
|
||||||
|
import TimeSeries from './TimeSeries';
|
||||||
|
import { splitQueryIntoOneChartPerQuery } from './utils';
|
||||||
|
|
||||||
|
function Explorer(): JSX.Element {
|
||||||
|
const {
|
||||||
|
handleRunQuery,
|
||||||
|
stagedQuery,
|
||||||
|
updateAllQueriesOperators,
|
||||||
|
currentQuery,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
|
const [showQuickFilters, setShowQuickFilters] = useState(true);
|
||||||
|
|
||||||
|
const defaultQuery = useMemo(
|
||||||
|
() =>
|
||||||
|
updateAllQueriesOperators(
|
||||||
|
initialQueryMeterWithType,
|
||||||
|
PANEL_TYPES.TIME_SERIES,
|
||||||
|
DataSource.METRICS,
|
||||||
|
'meter' as 'meter' | '',
|
||||||
|
),
|
||||||
|
[updateAllQueriesOperators],
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportDefaultQuery = useMemo(
|
||||||
|
() =>
|
||||||
|
updateAllQueriesOperators(
|
||||||
|
currentQuery || initialQueryMeterWithType,
|
||||||
|
PANEL_TYPES.TIME_SERIES,
|
||||||
|
DataSource.METRICS,
|
||||||
|
'meter' as 'meter' | '',
|
||||||
|
),
|
||||||
|
[currentQuery, updateAllQueriesOperators],
|
||||||
|
);
|
||||||
|
|
||||||
|
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||||
|
|
||||||
|
const handleExport = useCallback(
|
||||||
|
(
|
||||||
|
dashboard: Dashboard | null,
|
||||||
|
_isNewDashboard?: boolean,
|
||||||
|
queryToExport?: Query,
|
||||||
|
): void => {
|
||||||
|
if (!dashboard) return;
|
||||||
|
|
||||||
|
const widgetId = uuid();
|
||||||
|
|
||||||
|
const dashboardEditView = generateExportToDashboardLink({
|
||||||
|
query: queryToExport || exportDefaultQuery,
|
||||||
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
|
dashboardId: dashboard.id,
|
||||||
|
widgetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
safeNavigate(dashboardEditView);
|
||||||
|
},
|
||||||
|
[exportDefaultQuery, safeNavigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const splitedQueries = useMemo(
|
||||||
|
() =>
|
||||||
|
splitQueryIntoOneChartPerQuery(stagedQuery || initialQueryMeterWithType),
|
||||||
|
[stagedQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logEvent(MeterExplorerEvents.TabChanged, {
|
||||||
|
[MeterExplorerEventKeys.Tab]: 'explorer',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const queryComponents = useMemo(
|
||||||
|
(): QueryBuilderProps['queryComponents'] => ({}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
|
<div
|
||||||
|
className={cx('meter-explorer-container', {
|
||||||
|
'quick-filters-open': showQuickFilters,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cx('meter-explorer-quick-filters-section', {
|
||||||
|
hidden: !showQuickFilters,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<QuickFilters
|
||||||
|
className="qf-meter-explorer"
|
||||||
|
source={QuickFiltersSource.METER_EXPLORER}
|
||||||
|
signal={SignalType.METER_EXPLORER}
|
||||||
|
showFilterCollapse
|
||||||
|
showQueryName={false}
|
||||||
|
handleFilterVisibilityChange={(): void => {
|
||||||
|
setShowQuickFilters(!showQuickFilters);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="meter-explorer-content-section">
|
||||||
|
<div className="meter-explorer-explore-content">
|
||||||
|
<div className="explore-header">
|
||||||
|
<div className="explore-header-left-actions">
|
||||||
|
{!showQuickFilters && (
|
||||||
|
<Tooltip title="Show Quick Filters" placement="right" arrow={false}>
|
||||||
|
<Button
|
||||||
|
className="periscope-btn outline"
|
||||||
|
icon={<Filter size={16} />}
|
||||||
|
onClick={(): void => setShowQuickFilters(!showQuickFilters)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="explore-header-right-actions">
|
||||||
|
<DateTimeSelector showAutoRefresh />
|
||||||
|
<RightToolbarActions
|
||||||
|
onStageRunQuery={(): void => handleRunQuery(true, true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<QueryBuilderV2
|
||||||
|
config={{
|
||||||
|
initialDataSource: DataSource.METRICS,
|
||||||
|
queryVariant: 'static',
|
||||||
|
signalSource: 'meter',
|
||||||
|
}}
|
||||||
|
panelType={PANEL_TYPES.TIME_SERIES}
|
||||||
|
queryComponents={queryComponents}
|
||||||
|
showFunctions={false}
|
||||||
|
version="v3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="explore-content">
|
||||||
|
<TimeSeries />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ExplorerOptionWrapper
|
||||||
|
disabled={!stagedQuery}
|
||||||
|
query={exportDefaultQuery}
|
||||||
|
sourcepage={DataSource.METRICS}
|
||||||
|
signalSource="meter"
|
||||||
|
onExport={handleExport}
|
||||||
|
isOneChartPerQuery={false}
|
||||||
|
splitedQueries={splitedQueries}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Sentry.ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Explorer;
|
||||||
13
frontend/src/container/MeterExplorer/Explorer/NoData.tsx
Normal file
13
frontend/src/container/MeterExplorer/Explorer/NoData.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import { ChartLine } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function NoData(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="no-data-container">
|
||||||
|
<ChartLine size={48} />
|
||||||
|
<Typography.Text className="no-data-text">
|
||||||
|
No data found for the selected query
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
|
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
|
||||||
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
|
||||||
|
|
||||||
|
function QuerySection(): JSX.Element {
|
||||||
|
const { handleRunQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="query-section">
|
||||||
|
<QueryBuilder
|
||||||
|
panelType={panelTypes}
|
||||||
|
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
|
||||||
|
version="v4"
|
||||||
|
actions={
|
||||||
|
<ButtonWrapper>
|
||||||
|
<Button
|
||||||
|
onClick={(): void => {
|
||||||
|
handleRunQuery();
|
||||||
|
logEvent(MeterExplorerEvents.QueryBuilderQueryChanged, {
|
||||||
|
[MeterExplorerEventKeys.Tab]: 'explorer',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
Run Query
|
||||||
|
</Button>
|
||||||
|
</ButtonWrapper>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QuerySection;
|
||||||
142
frontend/src/container/MeterExplorer/Explorer/TimeSeries.tsx
Normal file
142
frontend/src/container/MeterExplorer/Explorer/TimeSeries.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { isAxiosError } from 'axios';
|
||||||
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
|
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
|
||||||
|
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||||
|
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useQueries } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
function TimeSeries(): JSX.Element {
|
||||||
|
const { stagedQuery, currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||||
|
AppState,
|
||||||
|
GlobalReducer
|
||||||
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
|
const [yAxisUnit, setYAxisUnit] = useState<string>('');
|
||||||
|
|
||||||
|
const isValidToConvertToMs = useMemo(() => {
|
||||||
|
const isValid: boolean[] = [];
|
||||||
|
|
||||||
|
currentQuery.builder.queryData.forEach(
|
||||||
|
({ aggregateAttribute, aggregateOperator }) => {
|
||||||
|
const isExistDurationNanoAttribute =
|
||||||
|
aggregateAttribute?.key === 'durationNano' ||
|
||||||
|
aggregateAttribute?.key === 'duration_nano';
|
||||||
|
|
||||||
|
const isCountOperator =
|
||||||
|
aggregateOperator === 'count' || aggregateOperator === 'count_distinct';
|
||||||
|
|
||||||
|
isValid.push(!isCountOperator && isExistDurationNanoAttribute);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return isValid.every(Boolean);
|
||||||
|
}, [currentQuery]);
|
||||||
|
|
||||||
|
const queryPayloads = useMemo(
|
||||||
|
() => [stagedQuery || initialQueryMeterWithType],
|
||||||
|
[stagedQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
|
const queries = useQueries(
|
||||||
|
queryPayloads.map((payload, index) => ({
|
||||||
|
queryKey: [
|
||||||
|
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||||
|
payload,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
|
globalSelectedTime,
|
||||||
|
maxTime,
|
||||||
|
minTime,
|
||||||
|
index,
|
||||||
|
],
|
||||||
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
GetMetricQueryRange(
|
||||||
|
{
|
||||||
|
query: payload,
|
||||||
|
graphType: PANEL_TYPES.TIME_SERIES,
|
||||||
|
selectedTime: 'GLOBAL_TIME',
|
||||||
|
globalSelectedInterval: globalSelectedTime,
|
||||||
|
params: {
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
|
),
|
||||||
|
enabled: !!payload,
|
||||||
|
retry: (failureCount: number, error: Error): boolean => {
|
||||||
|
let status: number | undefined;
|
||||||
|
|
||||||
|
if (error instanceof APIError) {
|
||||||
|
status = error.getHttpStatusCode();
|
||||||
|
} else if (isAxiosError(error)) {
|
||||||
|
status = error.response?.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status >= 400 && status < 500) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return failureCount < 3;
|
||||||
|
},
|
||||||
|
onError: (error: APIError): void => {
|
||||||
|
showErrorModal(error);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = useMemo(() => queries.map(({ data }) => data) ?? [], [queries]);
|
||||||
|
|
||||||
|
const responseData = useMemo(
|
||||||
|
() =>
|
||||||
|
data.map((datapoint) =>
|
||||||
|
isValidToConvertToMs ? convertDataValueToMs(datapoint) : datapoint,
|
||||||
|
),
|
||||||
|
[data, isValidToConvertToMs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onUnitChangeHandler = (value: string): void => {
|
||||||
|
setYAxisUnit(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="meter-time-series-container">
|
||||||
|
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
|
||||||
|
<div className="time-series-container">
|
||||||
|
{responseData.map((datapoint, index) => (
|
||||||
|
<div
|
||||||
|
className="time-series-view-panel"
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<TimeSeriesView
|
||||||
|
isFilterApplied={false}
|
||||||
|
isError={queries[index].isError}
|
||||||
|
isLoading={queries[index].isLoading}
|
||||||
|
data={datapoint}
|
||||||
|
dataSource={DataSource.METRICS}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeSeries;
|
||||||
3
frontend/src/container/MeterExplorer/Explorer/index.ts
Normal file
3
frontend/src/container/MeterExplorer/Explorer/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Explorer from './Explorer';
|
||||||
|
|
||||||
|
export default Explorer;
|
||||||
37
frontend/src/container/MeterExplorer/Explorer/types.ts
Normal file
37
frontend/src/container/MeterExplorer/Explorer/types.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
export enum ExplorerTabs {
|
||||||
|
TIME_SERIES = 'time-series',
|
||||||
|
RELATED_METRICS = 'related-metrics',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSeriesProps {
|
||||||
|
showOneChartPerQuery: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelatedMetricsProps {
|
||||||
|
metricNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelatedMetricsCardProps {
|
||||||
|
metric: RelatedMetricWithQueryResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseGetRelatedMetricsGraphsProps {
|
||||||
|
selectedMetricName: string | null;
|
||||||
|
startMs: number;
|
||||||
|
endMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseGetRelatedMetricsGraphsReturn {
|
||||||
|
relatedMetrics: RelatedMetricWithQueryResult[];
|
||||||
|
isRelatedMetricsLoading: boolean;
|
||||||
|
isRelatedMetricsError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelatedMetricWithQueryResult extends RelatedMetric {
|
||||||
|
queryResult: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>;
|
||||||
|
}
|
||||||
37
frontend/src/container/MeterExplorer/Explorer/utils.tsx
Normal file
37
frontend/src/container/MeterExplorer/Explorer/utils.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
export const splitQueryIntoOneChartPerQuery = (query: Query): Query[] => {
|
||||||
|
const queries: Query[] = [];
|
||||||
|
|
||||||
|
query.builder.queryData.forEach((currentQuery) => {
|
||||||
|
const newQuery = {
|
||||||
|
...query,
|
||||||
|
id: uuid(),
|
||||||
|
builder: {
|
||||||
|
...query.builder,
|
||||||
|
queryData: [currentQuery],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
queries.push(newQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
query.builder.queryFormulas.forEach((currentFormula) => {
|
||||||
|
const newQuery = {
|
||||||
|
...query,
|
||||||
|
id: uuid(),
|
||||||
|
builder: {
|
||||||
|
...query.builder,
|
||||||
|
queryFormulas: [currentFormula],
|
||||||
|
queryData: query.builder.queryData.map((currentQuery) => ({
|
||||||
|
...currentQuery,
|
||||||
|
disabled: true,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
queries.push(newQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
return queries;
|
||||||
|
};
|
||||||
51
frontend/src/container/MeterExplorer/events.ts
Normal file
51
frontend/src/container/MeterExplorer/events.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* This file contains all analytics events for the Meter Explorer.
|
||||||
|
*/
|
||||||
|
export enum MeterExplorerEvents {
|
||||||
|
TabChanged = 'Meter Explorer: Tab visited',
|
||||||
|
ModalOpened = 'Meter Explorer: Modal opened',
|
||||||
|
MeterClicked = 'Meter Explorer: Meter clicked',
|
||||||
|
FilterApplied = 'Meter Explorer: Filter applied',
|
||||||
|
TreemapViewChanged = 'Meter Explorer: Treemap view changed',
|
||||||
|
PageNumberChanged = 'Meter Explorer: Page number changed',
|
||||||
|
PageSizeChanged = 'Meter Explorer: Page size changed',
|
||||||
|
OrderByApplied = 'Meter Explorer: Order by applied',
|
||||||
|
MetricMetadataUpdated = 'Meter Explorer: Metric metadata updated',
|
||||||
|
OpenInExplorerClicked = 'Meter Explorer: Open in explorer clicked',
|
||||||
|
InspectViewChanged = 'Meter Explorer: Inspect view changed',
|
||||||
|
InspectQueryChanged = 'Meter Explorer: Inspect query changed',
|
||||||
|
InspectPointClicked = 'Meter Explorer: Inspect point clicked',
|
||||||
|
QueryBuilderQueryChanged = 'Meter Explorer: QueryBuilder query changed',
|
||||||
|
YAxisUnitApplied = 'Meter Explorer: Y axis unit applied',
|
||||||
|
AddToAlertClicked = 'Meter Explorer: Add to alert clicked',
|
||||||
|
AddToDashboardClicked = 'Meter Explorer: Add to dashboard clicked',
|
||||||
|
SaveViewClicked = 'Meter Explorer: Save view clicked',
|
||||||
|
SearchApplied = 'Meter Explorer: Search applied',
|
||||||
|
ViewEdited = 'Meter Explorer: View edited',
|
||||||
|
ViewDeleted = 'Meter Explorer: View deleted',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MeterExplorerEventKeys {
|
||||||
|
Tab = 'tab',
|
||||||
|
Modal = 'modal',
|
||||||
|
View = 'view',
|
||||||
|
Interval = 'interval',
|
||||||
|
ViewType = 'viewType',
|
||||||
|
PageNumber = 'pageNumber',
|
||||||
|
PageSize = 'pageSize',
|
||||||
|
ColumnName = 'columnName',
|
||||||
|
Order = 'order',
|
||||||
|
AttributeKey = 'attributeKey',
|
||||||
|
AttributeValue = 'attributeValue',
|
||||||
|
MetricName = 'metricName',
|
||||||
|
InspectView = 'inspectView',
|
||||||
|
TimeAggregationOption = 'timeAggregationOption',
|
||||||
|
TimeAggregationInterval = 'timeAggregationInterval',
|
||||||
|
SpaceAggregationOption = 'spaceAggregationOption',
|
||||||
|
SpaceAggregationLabels = 'spaceAggregationLabels',
|
||||||
|
OneChartPerQueryEnabled = 'oneChartPerQueryEnabled',
|
||||||
|
YAxisUnit = 'yAxisUnit',
|
||||||
|
ViewName = 'viewName',
|
||||||
|
Filters = 'filters',
|
||||||
|
TimeRange = 'timeRange',
|
||||||
|
}
|
||||||
@ -189,7 +189,7 @@ function Explorer(): JSX.Element {
|
|||||||
query={exportDefaultQuery}
|
query={exportDefaultQuery}
|
||||||
sourcepage={DataSource.METRICS}
|
sourcepage={DataSource.METRICS}
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
isOneChartPerQuery={showOneChartPerQuery}
|
isOneChartPerQuery={false}
|
||||||
splitedQueries={splitedQueries}
|
splitedQueries={splitedQueries}
|
||||||
/>
|
/>
|
||||||
</Sentry.ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import './MySettings.styles.scss';
|
import './MySettings.styles.scss';
|
||||||
|
|
||||||
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
|
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
|
||||||
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
@ -109,6 +110,9 @@ function MySettings(): JSX.Element {
|
|||||||
// Optimistically update the UI
|
// Optimistically update the UI
|
||||||
setSideNavPinned(checked);
|
setSideNavPinned(checked);
|
||||||
|
|
||||||
|
// Save to localStorage immediately for instant feedback
|
||||||
|
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, checked.toString());
|
||||||
|
|
||||||
// Update the context immediately
|
// Update the context immediately
|
||||||
const save = {
|
const save = {
|
||||||
name: USER_PREFERENCES.SIDENAV_PINNED,
|
name: USER_PREFERENCES.SIDENAV_PINNED,
|
||||||
@ -130,6 +134,8 @@ function MySettings(): JSX.Element {
|
|||||||
name: USER_PREFERENCES.SIDENAV_PINNED,
|
name: USER_PREFERENCES.SIDENAV_PINNED,
|
||||||
value: !checked,
|
value: !checked,
|
||||||
} as UserPreference);
|
} as UserPreference);
|
||||||
|
// Also revert localStorage
|
||||||
|
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, (!checked).toString());
|
||||||
showErrorNotification(notifications, error as AxiosError);
|
showErrorNotification(notifications, error as AxiosError);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKey
|
|||||||
import useDebounce from 'hooks/useDebounce';
|
import useDebounce from 'hooks/useDebounce';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
|
import { has } from 'lodash-es';
|
||||||
import { AllTraceFilterKeyValue } from 'pages/TracesExplorer/Filter/filterUtils';
|
import { AllTraceFilterKeyValue } from 'pages/TracesExplorer/Filter/filterUtils';
|
||||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
@ -452,7 +453,9 @@ const useOptionsMenu = ({
|
|||||||
() => ({
|
() => ({
|
||||||
addColumn: {
|
addColumn: {
|
||||||
isFetching: isSearchedAttributesFetchingV5,
|
isFetching: isSearchedAttributesFetchingV5,
|
||||||
value: preferences?.columns || defaultOptionsQuery.selectColumns,
|
value:
|
||||||
|
preferences?.columns.filter((item) => has(item, 'name')) ||
|
||||||
|
defaultOptionsQuery.selectColumns.filter((item) => has(item, 'name')),
|
||||||
options: optionsFromAttributeKeys || [],
|
options: optionsFromAttributeKeys || [],
|
||||||
onFocus: handleFocus,
|
onFocus: handleFocus,
|
||||||
onBlur: handleBlur,
|
onBlur: handleBlur,
|
||||||
|
|||||||
@ -17,8 +17,9 @@ export type QueryBuilderConfig =
|
|||||||
| {
|
| {
|
||||||
queryVariant: 'static';
|
queryVariant: 'static';
|
||||||
initialDataSource: DataSource;
|
initialDataSource: DataSource;
|
||||||
|
signalSource?: string;
|
||||||
}
|
}
|
||||||
| { queryVariant: 'dropdown' };
|
| { queryVariant: 'dropdown'; signalSource?: string };
|
||||||
|
|
||||||
export type QueryBuilderProps = {
|
export type QueryBuilderProps = {
|
||||||
config?: QueryBuilderConfig;
|
config?: QueryBuilderConfig;
|
||||||
|
|||||||
@ -11,4 +11,5 @@ export type QueryProps = {
|
|||||||
version: string;
|
version: string;
|
||||||
showSpanScopeSelector?: boolean;
|
showSpanScopeSelector?: boolean;
|
||||||
showOnlyWhereClause?: boolean;
|
showOnlyWhereClause?: boolean;
|
||||||
|
signalSource?: string;
|
||||||
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;
|
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;
|
||||||
|
|||||||
@ -8,4 +8,5 @@ export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
onSelect?: (value: BaseAutocompleteData) => void;
|
onSelect?: (value: BaseAutocompleteData) => void;
|
||||||
index?: number;
|
index?: number;
|
||||||
|
signalSource?: 'meter' | '';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
onSelect,
|
onSelect,
|
||||||
index,
|
index,
|
||||||
|
signalSource,
|
||||||
}: AgregatorFilterProps): JSX.Element {
|
}: AgregatorFilterProps): JSX.Element {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
||||||
@ -73,6 +74,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
searchText: debouncedValue,
|
searchText: debouncedValue,
|
||||||
aggregateOperator: queryAggregation.timeAggregation,
|
aggregateOperator: queryAggregation.timeAggregation,
|
||||||
dataSource: query.dataSource,
|
dataSource: query.dataSource,
|
||||||
|
source: signalSource || '',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
enabled:
|
enabled:
|
||||||
@ -152,10 +154,17 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
setSearchText(text);
|
setSearchText(text);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const placeholder: string =
|
const getPlaceholder = useCallback(() => {
|
||||||
query.dataSource === DataSource.METRICS
|
if (signalSource === 'meter') {
|
||||||
? `Search metric name`
|
return 'Meter name';
|
||||||
: 'Aggregate attribute';
|
}
|
||||||
|
|
||||||
|
if (query.dataSource === DataSource.METRICS) {
|
||||||
|
return 'Metric name';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Aggregate attribute';
|
||||||
|
}, [signalSource, query.dataSource]);
|
||||||
|
|
||||||
const getAttributesData = useCallback(
|
const getAttributesData = useCallback(
|
||||||
(): BaseAutocompleteData[] =>
|
(): BaseAutocompleteData[] =>
|
||||||
@ -289,7 +298,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
return (
|
return (
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
placeholder={placeholder}
|
placeholder={getPlaceholder()}
|
||||||
style={selectStyle}
|
style={selectStyle}
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
onSearch={handleSearchText}
|
onSearch={handleSearchText}
|
||||||
|
|||||||
@ -30,8 +30,10 @@ function BuilderUnitsFilter({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space className="builder-units-filter">
|
||||||
<DefaultLabel>Y-axis unit</DefaultLabel>
|
<DefaultLabel className="builder-units-filter-label">
|
||||||
|
Y-axis unit
|
||||||
|
</DefaultLabel>
|
||||||
<Select
|
<Select
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
style={selectStyles}
|
style={selectStyles}
|
||||||
|
|||||||
@ -5,4 +5,5 @@ export type GroupByFilterProps = {
|
|||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
onChange: (values: BaseAutocompleteData[]) => void;
|
onChange: (values: BaseAutocompleteData[]) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
signalSource?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,9 +10,17 @@ import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAut
|
|||||||
// ** Helpers
|
// ** Helpers
|
||||||
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
import { isEqual, uniqWith } from 'lodash-es';
|
import { isEqual, uniqWith } from 'lodash-es';
|
||||||
import { memo, ReactNode, useCallback, useEffect, useState } from 'react';
|
import {
|
||||||
|
memo,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { SelectOption } from 'types/common/select';
|
import { SelectOption } from 'types/common/select';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
@ -25,6 +33,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
|||||||
query,
|
query,
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
|
signalSource,
|
||||||
}: GroupByFilterProps): JSX.Element {
|
}: GroupByFilterProps): JSX.Element {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [searchText, setSearchText] = useState<string>('');
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
@ -38,10 +47,17 @@ export const GroupByFilter = memo(function GroupByFilter({
|
|||||||
|
|
||||||
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
|
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
|
||||||
|
|
||||||
|
const dataSource = useMemo(() => {
|
||||||
|
if (signalSource === 'meter') {
|
||||||
|
return 'meter' as DataSource;
|
||||||
|
}
|
||||||
|
return query.dataSource;
|
||||||
|
}, [signalSource, query.dataSource]);
|
||||||
|
|
||||||
const { isFetching } = useGetAggregateKeys(
|
const { isFetching } = useGetAggregateKeys(
|
||||||
{
|
{
|
||||||
aggregateAttribute: query.aggregateAttribute?.key || '',
|
aggregateAttribute: query.aggregateAttribute?.key || '',
|
||||||
dataSource: query.dataSource,
|
dataSource,
|
||||||
aggregateOperator: query.aggregateOperator || '',
|
aggregateOperator: query.aggregateOperator || '',
|
||||||
searchText: debouncedValue,
|
searchText: debouncedValue,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
Book,
|
Book,
|
||||||
Boxes,
|
Boxes,
|
||||||
BugIcon,
|
BugIcon,
|
||||||
|
ChartArea,
|
||||||
Cloudy,
|
Cloudy,
|
||||||
DraftingCompass,
|
DraftingCompass,
|
||||||
FileKey2,
|
FileKey2,
|
||||||
@ -113,7 +114,7 @@ const menuItems: SidebarItem[] = [
|
|||||||
key: ROUTES.METRICS_EXPLORER,
|
key: ROUTES.METRICS_EXPLORER,
|
||||||
label: 'Metrics',
|
label: 'Metrics',
|
||||||
icon: <BarChart2 size={16} />,
|
icon: <BarChart2 size={16} />,
|
||||||
isNew: true,
|
isNew: false,
|
||||||
itemKey: 'metrics',
|
itemKey: 'metrics',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -230,7 +231,7 @@ export const defaultMoreMenuItems: SidebarItem[] = [
|
|||||||
key: ROUTES.METRICS_EXPLORER,
|
key: ROUTES.METRICS_EXPLORER,
|
||||||
label: 'Metrics',
|
label: 'Metrics',
|
||||||
icon: <BarChart2 size={16} />,
|
icon: <BarChart2 size={16} />,
|
||||||
isNew: true,
|
isNew: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
itemKey: 'metrics',
|
itemKey: 'metrics',
|
||||||
},
|
},
|
||||||
@ -264,6 +265,15 @@ export const defaultMoreMenuItems: SidebarItem[] = [
|
|||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
itemKey: 'external-apis',
|
itemKey: 'external-apis',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: ROUTES.METER_EXPLORER,
|
||||||
|
label: 'Meter Explorer',
|
||||||
|
icon: <ChartArea size={16} />,
|
||||||
|
isNew: false,
|
||||||
|
isEnabled: false,
|
||||||
|
isBeta: true,
|
||||||
|
itemKey: 'meter-explorer',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
|
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
|
||||||
label: 'Messaging Queues',
|
label: 'Messaging Queues',
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.span-details-drawer {
|
.span-details-drawer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 330px;
|
width: 450px;
|
||||||
border-left: 1px solid var(--bg-slate-400);
|
border-left: 1px solid var(--bg-slate-400);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
@ -176,6 +176,34 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(171, 189, 255, 0.1);
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-variant-numeric: lining-nums tabular-nums slashed-zero;
|
||||||
|
font-feature-settings: 'dlig' on, 'salt' on;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: -0.065px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attributes-tab-btn:hover,
|
.attributes-tab-btn:hover,
|
||||||
|
|||||||
@ -54,7 +54,10 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
|||||||
icon={<Bookmark size="14" />}
|
icon={<Bookmark size="14" />}
|
||||||
className="attributes-tab-btn"
|
className="attributes-tab-btn"
|
||||||
>
|
>
|
||||||
Attributes
|
<span className="tab-label">Attributes</span>
|
||||||
|
<span className="count-badge">
|
||||||
|
{Object.keys(span.tagMap || {}).length}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
key: 'attributes',
|
key: 'attributes',
|
||||||
@ -63,7 +66,8 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
|||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<Button type="text" icon={<Anvil size="14" />} className="events-tab-btn">
|
<Button type="text" icon={<Anvil size="14" />} className="events-tab-btn">
|
||||||
Events
|
<span className="tab-label">Events</span>
|
||||||
|
<span className="count-badge">{span.event?.length || 0}</span>
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
key: 'events',
|
key: 'events',
|
||||||
@ -82,7 +86,14 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
|||||||
icon={<Link2 size="14" />}
|
icon={<Link2 size="14" />}
|
||||||
className="linked-spans-tab-btn"
|
className="linked-spans-tab-btn"
|
||||||
>
|
>
|
||||||
Links
|
<span className="tab-label">Links</span>
|
||||||
|
<span className="count-badge">
|
||||||
|
{
|
||||||
|
(
|
||||||
|
span.references?.filter((ref: any) => ref.refType !== 'CHILD_OF') || []
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
key: 'linked-spans',
|
key: 'linked-spans',
|
||||||
|
|||||||
@ -205,6 +205,7 @@ function TimeSeriesView({
|
|||||||
return (
|
return (
|
||||||
<div className="time-series-view">
|
<div className="time-series-view">
|
||||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="graph-container"
|
className="graph-container"
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
|||||||
@ -47,7 +47,7 @@ function TimeSeriesViewContainer({
|
|||||||
return isValid.every(Boolean);
|
return isValid.every(Boolean);
|
||||||
}, [currentQuery]);
|
}, [currentQuery]);
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useGetQueryRange(
|
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||||
{
|
{
|
||||||
query: stagedQuery || initialQueriesMap[dataSource],
|
query: stagedQuery || initialQueriesMap[dataSource],
|
||||||
graphType: panelType || PANEL_TYPES.TIME_SERIES,
|
graphType: panelType || PANEL_TYPES.TIME_SERIES,
|
||||||
@ -88,7 +88,7 @@ function TimeSeriesViewContainer({
|
|||||||
isFilterApplied={isFilterApplied}
|
isFilterApplied={isFilterApplied}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
error={error as APIError}
|
error={error as APIError}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading || isFetching}
|
||||||
data={responseData}
|
data={responseData}
|
||||||
yAxisUnit={isValidToConvertToMs ? 'ms' : 'short'}
|
yAxisUnit={isValidToConvertToMs ? 'ms' : 'short'}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
|
|||||||
@ -233,6 +233,9 @@ export const routesToSkip = [
|
|||||||
ROUTES.ALL_ERROR,
|
ROUTES.ALL_ERROR,
|
||||||
ROUTES.UN_AUTHORIZED,
|
ROUTES.UN_AUTHORIZED,
|
||||||
ROUTES.NOT_FOUND,
|
ROUTES.NOT_FOUND,
|
||||||
|
ROUTES.METER_EXPLORER,
|
||||||
|
ROUTES.METER_EXPLORER_BASE,
|
||||||
|
ROUTES.METER_EXPLORER_VIEWS,
|
||||||
ROUTES.SOMETHING_WENT_WRONG,
|
ROUTES.SOMETHING_WENT_WRONG,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -372,7 +372,7 @@ function DateTimeSelection({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return JSON.stringify(updatedCompositeQuery);
|
return encodeURIComponent(JSON.stringify(updatedCompositeQuery));
|
||||||
}, [currentQuery]);
|
}, [currentQuery]);
|
||||||
|
|
||||||
const onSelectHandler = useCallback(
|
const onSelectHandler = useCallback(
|
||||||
|
|||||||
@ -1,22 +1,44 @@
|
|||||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
import { useQuery, UseQueryResult } from 'react-query';
|
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||||
|
import { ErrorResponse } from 'react-router-dom-v5-compat';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
import { QueryKeyValueSuggestionsResponseProps } from 'types/api/querySuggestions/types';
|
import { QueryKeyValueSuggestionsResponseProps } from 'types/api/querySuggestions/types';
|
||||||
|
|
||||||
export const useGetQueryKeyValueSuggestions = ({
|
export const useGetQueryKeyValueSuggestions = ({
|
||||||
key,
|
key,
|
||||||
signal,
|
signal,
|
||||||
searchText,
|
searchText,
|
||||||
|
signalSource,
|
||||||
|
metricName,
|
||||||
}: {
|
}: {
|
||||||
key: string;
|
key: string;
|
||||||
signal: 'traces' | 'logs' | 'metrics';
|
signal: 'traces' | 'logs' | 'metrics';
|
||||||
searchText?: string;
|
searchText?: string;
|
||||||
|
signalSource?: 'meter' | '';
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
SuccessResponse<QueryKeyValueSuggestionsResponseProps> | ErrorResponse
|
||||||
|
>;
|
||||||
|
metricName?: string;
|
||||||
}): UseQueryResult<
|
}): UseQueryResult<
|
||||||
AxiosResponse<QueryKeyValueSuggestionsResponseProps>,
|
AxiosResponse<QueryKeyValueSuggestionsResponseProps>,
|
||||||
AxiosError
|
AxiosError
|
||||||
> =>
|
> =>
|
||||||
useQuery<AxiosResponse<QueryKeyValueSuggestionsResponseProps>, AxiosError>({
|
useQuery<AxiosResponse<QueryKeyValueSuggestionsResponseProps>, AxiosError>({
|
||||||
queryKey: ['queryKeyValueSuggestions', key, signal, searchText],
|
queryKey: [
|
||||||
|
'queryKeyValueSuggestions',
|
||||||
|
key,
|
||||||
|
signal,
|
||||||
|
searchText,
|
||||||
|
signalSource,
|
||||||
|
metricName,
|
||||||
|
],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
getValueSuggestions({ signal, key, searchText: searchText || '' }),
|
getValueSuggestions({
|
||||||
|
signal,
|
||||||
|
key,
|
||||||
|
searchText: searchText || '',
|
||||||
|
signalSource: signalSource as 'meter' | '',
|
||||||
|
metricName: metricName || '',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { AllViewsProps } from 'types/api/saveViews/types';
|
|||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export const useGetAllViews = (
|
export const useGetAllViews = (
|
||||||
sourcepage: DataSource,
|
sourcepage: DataSource | 'meter',
|
||||||
): UseQueryResult<AxiosResponse<AllViewsProps>, AxiosError> =>
|
): UseQueryResult<AxiosResponse<AllViewsProps>, AxiosError> =>
|
||||||
useQuery<AxiosResponse<AllViewsProps>, AxiosError>({
|
useQuery<AxiosResponse<AllViewsProps>, AxiosError>({
|
||||||
queryKey: [{ sourcepage }],
|
queryKey: [{ sourcepage }],
|
||||||
queryFn: () => getAllViews(sourcepage),
|
queryFn: () => getAllViews(sourcepage as DataSource),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -17,9 +17,9 @@ const getChartData = ({
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
} => {
|
} => {
|
||||||
const uniqueTimeLabels = new Set<number>();
|
const uniqueTimeLabels = new Set<number>();
|
||||||
queryData.forEach((data) => {
|
queryData?.forEach((data) => {
|
||||||
data.queryData.forEach((query) => {
|
data.queryData?.forEach((query) => {
|
||||||
query.values.forEach((value) => {
|
query.values?.forEach((value) => {
|
||||||
uniqueTimeLabels.add(value[0]);
|
uniqueTimeLabels.add(value[0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -27,8 +27,8 @@ const getChartData = ({
|
|||||||
|
|
||||||
const labels = Array.from(uniqueTimeLabels).sort((a, b) => a - b);
|
const labels = Array.from(uniqueTimeLabels).sort((a, b) => a - b);
|
||||||
|
|
||||||
const response = queryData.map(
|
const response =
|
||||||
({ queryData, query: queryG, legend: legendG }) =>
|
queryData?.map(({ queryData, query: queryG, legend: legendG }) =>
|
||||||
queryData.map((e) => {
|
queryData.map((e) => {
|
||||||
const { values = [], metric, legend, queryName } = e || {};
|
const { values = [], metric, legend, queryName } = e || {};
|
||||||
const labelNames = getLabelName(
|
const labelNames = getLabelName(
|
||||||
@ -61,7 +61,7 @@ const getChartData = ({
|
|||||||
second: filledDataValues.map((e) => e.second || 0),
|
second: filledDataValues.map((e) => e.second || 0),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
) || [];
|
||||||
|
|
||||||
const modifiedData = response
|
const modifiedData = response
|
||||||
.flat()
|
.flat()
|
||||||
|
|||||||
@ -490,6 +490,7 @@ export const defaultOutput = {
|
|||||||
pageSize: 0,
|
pageSize: 0,
|
||||||
queryName: 'A',
|
queryName: 'A',
|
||||||
reduceTo: 'avg',
|
reduceTo: 'avg',
|
||||||
|
source: '',
|
||||||
spaceAggregation: 'sum',
|
spaceAggregation: 'sum',
|
||||||
stepInterval: 240,
|
stepInterval: 240,
|
||||||
timeAggregation: 'rate',
|
timeAggregation: 'rate',
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import cx from 'classnames';
|
|||||||
import { CardContainer } from 'container/GridCardLayout/styles';
|
import { CardContainer } from 'container/GridCardLayout/styles';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
@ -129,23 +129,22 @@ function MetricPage(): JSX.Element {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const [renderedGraphCount, setRenderedGraphCount] = useState(0);
|
const renderedGraphCountRef = useRef(0);
|
||||||
const hasLoggedRef = useRef(false);
|
const hasLoggedRef = useRef(false);
|
||||||
|
|
||||||
const checkIfDataExists = (isDataAvailable: boolean): void => {
|
const checkIfDataExists = useCallback((isDataAvailable: boolean): void => {
|
||||||
if (isDataAvailable) {
|
if (isDataAvailable) {
|
||||||
const newCount = renderedGraphCount + 1;
|
renderedGraphCountRef.current += 1;
|
||||||
setRenderedGraphCount(newCount);
|
|
||||||
|
|
||||||
// Only log when first graph has rendered and we haven't logged yet
|
// Only log when first graph has rendered and we haven't logged yet
|
||||||
if (newCount === 1 && !hasLoggedRef.current) {
|
if (renderedGraphCountRef.current === 1 && !hasLoggedRef.current) {
|
||||||
logEvent('MQ Kafka: Metric view', {
|
logEvent('MQ Kafka: Metric view', {
|
||||||
graphRendered: true,
|
graphRendered: true,
|
||||||
});
|
});
|
||||||
hasLoggedRef.current = true;
|
hasLoggedRef.current = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metric-page">
|
<div className="metric-page">
|
||||||
|
|||||||
16
frontend/src/pages/MeterExplorer/MeterExplorer.styles.scss
Normal file
16
frontend/src/pages/MeterExplorer/MeterExplorer.styles.scss
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.meter-explorer-page {
|
||||||
|
.ant-tabs-nav {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab {
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 0 8px 0 0 !important;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
frontend/src/pages/MeterExplorer/MeterExplorerPage.tsx
Normal file
22
frontend/src/pages/MeterExplorer/MeterExplorerPage.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import './MeterExplorer.styles.scss';
|
||||||
|
|
||||||
|
import RouteTab from 'components/RouteTab';
|
||||||
|
import { TabRoutes } from 'components/RouteTab/types';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
|
import { Explorer, Views } from './constants';
|
||||||
|
|
||||||
|
function MeterExplorerPage(): JSX.Element {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const routes: TabRoutes[] = [Explorer, Views];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="meter-explorer-page">
|
||||||
|
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeterExplorerPage;
|
||||||
32
frontend/src/pages/MeterExplorer/constants.tsx
Normal file
32
frontend/src/pages/MeterExplorer/constants.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { TabRoutes } from 'components/RouteTab/types';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import ExplorerPage from 'container/MeterExplorer/Explorer';
|
||||||
|
import { Compass, TowerControl } from 'lucide-react';
|
||||||
|
import SaveView from 'pages/SaveView';
|
||||||
|
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||||
|
|
||||||
|
export const Explorer: TabRoutes = {
|
||||||
|
Component: (): JSX.Element => (
|
||||||
|
<PreferenceContextProvider>
|
||||||
|
<ExplorerPage />
|
||||||
|
</PreferenceContextProvider>
|
||||||
|
),
|
||||||
|
name: (
|
||||||
|
<div className="tab-item">
|
||||||
|
<Compass size={16} /> Explorer
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
route: ROUTES.METER_EXPLORER,
|
||||||
|
key: ROUTES.METER_EXPLORER,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Views: TabRoutes = {
|
||||||
|
Component: SaveView,
|
||||||
|
name: (
|
||||||
|
<div className="tab-item">
|
||||||
|
<TowerControl size={16} /> Views
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
route: ROUTES.METER_EXPLORER_VIEWS,
|
||||||
|
key: ROUTES.METER_EXPLORER_VIEWS,
|
||||||
|
};
|
||||||
3
frontend/src/pages/MeterExplorer/index.tsx
Normal file
3
frontend/src/pages/MeterExplorer/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import MeterExplorerPage from './MeterExplorerPage';
|
||||||
|
|
||||||
|
export default MeterExplorerPage;
|
||||||
@ -6,6 +6,7 @@ export const SOURCEPAGE_VS_ROUTES: {
|
|||||||
logs: ROUTES.LOGS_EXPLORER,
|
logs: ROUTES.LOGS_EXPLORER,
|
||||||
traces: ROUTES.TRACES_EXPLORER,
|
traces: ROUTES.TRACES_EXPLORER,
|
||||||
metrics: ROUTES.METRICS_EXPLORER_EXPLORER,
|
metrics: ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||||
|
meter: ROUTES.METER_EXPLORER,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ROUTES_VS_SOURCEPAGE: {
|
export const ROUTES_VS_SOURCEPAGE: {
|
||||||
@ -14,4 +15,5 @@ export const ROUTES_VS_SOURCEPAGE: {
|
|||||||
[ROUTES.LOGS_SAVE_VIEWS]: 'logs',
|
[ROUTES.LOGS_SAVE_VIEWS]: 'logs',
|
||||||
[ROUTES.TRACES_SAVE_VIEWS]: 'traces',
|
[ROUTES.TRACES_SAVE_VIEWS]: 'traces',
|
||||||
[ROUTES.METRICS_EXPLORER_VIEWS]: 'metrics',
|
[ROUTES.METRICS_EXPLORER_VIEWS]: 'metrics',
|
||||||
|
[ROUTES.METER_EXPLORER_VIEWS]: 'meter',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@ -17,6 +17,10 @@ import {
|
|||||||
} from 'components/ExplorerCard/utils';
|
} from 'components/ExplorerCard/utils';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import { getRandomColor } from 'container/ExplorerOptions/utils';
|
import { getRandomColor } from 'container/ExplorerOptions/utils';
|
||||||
|
import {
|
||||||
|
MeterExplorerEventKeys,
|
||||||
|
MeterExplorerEvents,
|
||||||
|
} from 'container/MeterExplorer/events';
|
||||||
import {
|
import {
|
||||||
MetricsExplorerEventKeys,
|
MetricsExplorerEventKeys,
|
||||||
MetricsExplorerEvents,
|
MetricsExplorerEvents,
|
||||||
@ -163,6 +167,10 @@ function SaveView(): JSX.Element {
|
|||||||
logEvent(MetricsExplorerEvents.TabChanged, {
|
logEvent(MetricsExplorerEvents.TabChanged, {
|
||||||
[MetricsExplorerEventKeys.Tab]: 'views',
|
[MetricsExplorerEventKeys.Tab]: 'views',
|
||||||
});
|
});
|
||||||
|
} else if (sourcepage === 'meter') {
|
||||||
|
logEvent(MeterExplorerEvents.TabChanged, {
|
||||||
|
[MeterExplorerEventKeys.Tab]: 'views',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -241,12 +241,26 @@ export function QueryBuilderProvider({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const updateAllQueriesOperators = useCallback(
|
const updateAllQueriesOperators = useCallback(
|
||||||
(query: Query, panelType: PANEL_TYPES, dataSource: DataSource): Query => {
|
(
|
||||||
|
query: Query,
|
||||||
|
panelType: PANEL_TYPES,
|
||||||
|
dataSource: DataSource,
|
||||||
|
signalSource?: 'meter' | '',
|
||||||
|
): Query => {
|
||||||
const queryData = query.builder.queryData?.map((item) =>
|
const queryData = query.builder.queryData?.map((item) =>
|
||||||
getElementWithActualOperator(item, dataSource, panelType),
|
getElementWithActualOperator(item, dataSource, panelType),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ...query, builder: { ...query.builder, queryData } };
|
return {
|
||||||
|
...query,
|
||||||
|
builder: {
|
||||||
|
...query.builder,
|
||||||
|
queryData: queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
source: signalSource,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
[getElementWithActualOperator],
|
[getElementWithActualOperator],
|
||||||
@ -854,6 +868,7 @@ export function QueryBuilderProvider({
|
|||||||
const handleRunQuery = useCallback(
|
const handleRunQuery = useCallback(
|
||||||
(shallUpdateStepInterval?: boolean, newQBQuery?: boolean) => {
|
(shallUpdateStepInterval?: boolean, newQBQuery?: boolean) => {
|
||||||
let currentQueryData = currentQuery;
|
let currentQueryData = currentQuery;
|
||||||
|
|
||||||
if (newQBQuery) {
|
if (newQBQuery) {
|
||||||
currentQueryData = {
|
currentQueryData = {
|
||||||
...currentQueryData,
|
...currentQueryData,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
/* eslint-disable no-empty */
|
/* eslint-disable no-empty */
|
||||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||||
|
import { has } from 'lodash-es';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
@ -8,6 +9,14 @@ import logsLoaderConfig from '../configs/logsLoaderConfig';
|
|||||||
import tracesLoaderConfig from '../configs/tracesLoaderConfig';
|
import tracesLoaderConfig from '../configs/tracesLoaderConfig';
|
||||||
import { FormattingOptions, Preferences } from '../types';
|
import { FormattingOptions, Preferences } from '../types';
|
||||||
|
|
||||||
|
const migrateColumns = (columns: any): any =>
|
||||||
|
columns.map((column: any) => {
|
||||||
|
if (has(column, 'key') && !has(column, 'name')) {
|
||||||
|
return { ...column, name: column.key };
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
|
||||||
// Generic preferences loader that works with any config
|
// Generic preferences loader that works with any config
|
||||||
async function preferencesLoader<T>(config: {
|
async function preferencesLoader<T>(config: {
|
||||||
priority: readonly string[];
|
priority: readonly string[];
|
||||||
@ -26,11 +35,16 @@ async function preferencesLoader<T>(config: {
|
|||||||
const validColumnsResult = results.find(
|
const validColumnsResult = results.find(
|
||||||
({ result }) => result.columns?.length,
|
({ result }) => result.columns?.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
const validFormattingResult = results.find(({ result }) => result.formatting);
|
const validFormattingResult = results.find(({ result }) => result.formatting);
|
||||||
|
|
||||||
|
const migratedColumns = validColumnsResult?.result.columns
|
||||||
|
? migrateColumns(validColumnsResult?.result.columns)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Combine valid results or fallback to default
|
// Combine valid results or fallback to default
|
||||||
const finalResult = {
|
const finalResult = {
|
||||||
columns: validColumnsResult?.result.columns || config.default().columns,
|
columns: migratedColumns || config.default().columns,
|
||||||
formatting:
|
formatting:
|
||||||
validFormattingResult?.result.formatting || config.default().formatting,
|
validFormattingResult?.result.formatting || config.default().formatting,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,4 +4,5 @@ export interface IGetAggregateAttributePayload {
|
|||||||
aggregateOperator: string;
|
aggregateOperator: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
|
source?: 'meter' | '';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,7 @@ export type IBuilderQuery = {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
selectColumns?: BaseAutocompleteData[] | TelemetryFieldKey[];
|
selectColumns?: BaseAutocompleteData[] | TelemetryFieldKey[];
|
||||||
|
source?: 'meter' | '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IClickHouseQuery {
|
export interface IClickHouseQuery {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export interface QueryKeyRequestProps {
|
|||||||
fieldContext?: 'resource' | 'scope' | 'attribute' | 'span';
|
fieldContext?: 'resource' | 'scope' | 'attribute' | 'span';
|
||||||
fieldDataType?: QUERY_BUILDER_KEY_TYPES;
|
fieldDataType?: QUERY_BUILDER_KEY_TYPES;
|
||||||
metricName?: string;
|
metricName?: string;
|
||||||
|
signalSource?: 'meter' | '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryKeyValueSuggestionsProps {
|
export interface QueryKeyValueSuggestionsProps {
|
||||||
@ -44,4 +45,8 @@ export interface QueryKeyValueRequestProps {
|
|||||||
signal: 'traces' | 'logs' | 'metrics';
|
signal: 'traces' | 'logs' | 'metrics';
|
||||||
key: string;
|
key: string;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
|
signalSource?: 'meter' | '';
|
||||||
|
metricName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SignalType = 'traces' | 'logs' | 'metrics';
|
||||||
|
|||||||
@ -239,10 +239,17 @@ export interface MetricBuilderQuery extends BaseBuilderQuery {
|
|||||||
aggregations?: MetricAggregation[];
|
aggregations?: MetricAggregation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MeterBuilderQuery extends BaseBuilderQuery {
|
||||||
|
signal: 'metrics';
|
||||||
|
source: 'meter';
|
||||||
|
aggregations?: MetricAggregation[];
|
||||||
|
}
|
||||||
|
|
||||||
export type BuilderQuery =
|
export type BuilderQuery =
|
||||||
| TraceBuilderQuery
|
| TraceBuilderQuery
|
||||||
| LogBuilderQuery
|
| LogBuilderQuery
|
||||||
| MetricBuilderQuery;
|
| MetricBuilderQuery
|
||||||
|
| MeterBuilderQuery;
|
||||||
|
|
||||||
export interface QueryBuilderFormula {
|
export interface QueryBuilderFormula {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -105,6 +105,42 @@ export enum MetricAggregateOperator {
|
|||||||
LATEST = 'latest',
|
LATEST = 'latest',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum MeterAggregateOperator {
|
||||||
|
EMPTY = '', // used as time aggregator for histograms
|
||||||
|
NOOP = 'noop',
|
||||||
|
COUNT = 'count',
|
||||||
|
COUNT_DISTINCT = 'count_distinct',
|
||||||
|
SUM = 'sum',
|
||||||
|
AVG = 'avg',
|
||||||
|
MAX = 'max',
|
||||||
|
MIN = 'min',
|
||||||
|
P05 = 'p05',
|
||||||
|
P10 = 'p10',
|
||||||
|
P20 = 'p20',
|
||||||
|
P25 = 'p25',
|
||||||
|
P50 = 'p50',
|
||||||
|
P75 = 'p75',
|
||||||
|
P90 = 'p90',
|
||||||
|
P95 = 'p95',
|
||||||
|
P99 = 'p99',
|
||||||
|
RATE = 'rate',
|
||||||
|
SUM_RATE = 'sum_rate',
|
||||||
|
AVG_RATE = 'avg_rate',
|
||||||
|
MAX_RATE = 'max_rate',
|
||||||
|
MIN_RATE = 'min_rate',
|
||||||
|
RATE_SUM = 'rate_sum',
|
||||||
|
RATE_AVG = 'rate_avg',
|
||||||
|
RATE_MIN = 'rate_min',
|
||||||
|
RATE_MAX = 'rate_max',
|
||||||
|
HIST_QUANTILE_50 = 'hist_quantile_50',
|
||||||
|
HIST_QUANTILE_75 = 'hist_quantile_75',
|
||||||
|
HIST_QUANTILE_90 = 'hist_quantile_90',
|
||||||
|
HIST_QUANTILE_95 = 'hist_quantile_95',
|
||||||
|
HIST_QUANTILE_99 = 'hist_quantile_99',
|
||||||
|
INCREASE = 'increase',
|
||||||
|
LATEST = 'latest',
|
||||||
|
}
|
||||||
|
|
||||||
export enum TracesAggregatorOperator {
|
export enum TracesAggregatorOperator {
|
||||||
NOOP = 'noop',
|
NOOP = 'noop',
|
||||||
COUNT = 'count',
|
COUNT = 'count',
|
||||||
@ -237,6 +273,7 @@ export type QueryBuilderContextType = {
|
|||||||
queryData: Query,
|
queryData: Query,
|
||||||
panelType: PANEL_TYPES,
|
panelType: PANEL_TYPES,
|
||||||
dataSource: DataSource,
|
dataSource: DataSource,
|
||||||
|
signalSource?: 'meter' | '',
|
||||||
) => Query;
|
) => Query;
|
||||||
updateQueriesData: <T extends keyof QueryBuilderData>(
|
updateQueriesData: <T extends keyof QueryBuilderData>(
|
||||||
query: Query,
|
query: Query,
|
||||||
|
|||||||
@ -123,4 +123,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
|||||||
INFRASTRUCTURE_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
INFRASTRUCTURE_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
API_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
API_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
MESSAGING_QUEUES_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
MESSAGING_QUEUES_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
METER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
METER_EXPLORER_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
METER_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrymetadata"
|
"github.com/SigNoz/signoz/pkg/telemetrymetadata"
|
||||||
|
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
||||||
@ -30,12 +31,17 @@ func NewAPI(
|
|||||||
telemetryStore,
|
telemetryStore,
|
||||||
telemetrytraces.DBName,
|
telemetrytraces.DBName,
|
||||||
telemetrytraces.TagAttributesV2TableName,
|
telemetrytraces.TagAttributesV2TableName,
|
||||||
|
telemetrytraces.SpanAttributesKeysTblName,
|
||||||
telemetrytraces.SpanIndexV3TableName,
|
telemetrytraces.SpanIndexV3TableName,
|
||||||
telemetrymetrics.DBName,
|
telemetrymetrics.DBName,
|
||||||
telemetrymetrics.AttributesMetadataTableName,
|
telemetrymetrics.AttributesMetadataTableName,
|
||||||
|
telemetrymeter.DBName,
|
||||||
|
telemetrymeter.SamplesAgg1dTableName,
|
||||||
telemetrylogs.DBName,
|
telemetrylogs.DBName,
|
||||||
telemetrylogs.LogsV2TableName,
|
telemetrylogs.LogsV2TableName,
|
||||||
telemetrylogs.TagAttributesV2TableName,
|
telemetrylogs.TagAttributesV2TableName,
|
||||||
|
telemetrylogs.LogAttributeKeysTblName,
|
||||||
|
telemetrylogs.LogResourceKeysTblName,
|
||||||
telemetrymetadata.DBName,
|
telemetrymetadata.DBName,
|
||||||
telemetrymetadata.AttributesMetadataLocalTableName,
|
telemetrymetadata.AttributesMetadataLocalTableName,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, error) {
|
func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, error) {
|
||||||
var req telemetrytypes.FieldKeySelector
|
var req telemetrytypes.FieldKeySelector
|
||||||
var signal telemetrytypes.Signal
|
var signal telemetrytypes.Signal
|
||||||
|
var source telemetrytypes.Source
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
signalStr := r.URL.Query().Get("signal")
|
signalStr := r.URL.Query().Get("signal")
|
||||||
@ -21,6 +22,13 @@ func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, er
|
|||||||
signal = telemetrytypes.SignalUnspecified
|
signal = telemetrytypes.SignalUnspecified
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceStr := r.URL.Query().Get("source")
|
||||||
|
if sourceStr != "" {
|
||||||
|
source = telemetrytypes.Source{String: valuer.NewString(sourceStr)}
|
||||||
|
} else {
|
||||||
|
source = telemetrytypes.SourceUnspecified
|
||||||
|
}
|
||||||
|
|
||||||
if r.URL.Query().Get("limit") != "" {
|
if r.URL.Query().Get("limit") != "" {
|
||||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -76,6 +84,7 @@ func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, er
|
|||||||
StartUnixMilli: startUnixMilli,
|
StartUnixMilli: startUnixMilli,
|
||||||
EndUnixMilli: endUnixMilli,
|
EndUnixMilli: endUnixMilli,
|
||||||
Signal: signal,
|
Signal: signal,
|
||||||
|
Source: source,
|
||||||
Name: name,
|
Name: name,
|
||||||
FieldContext: fieldContext,
|
FieldContext: fieldContext,
|
||||||
FieldDataType: fieldDataType,
|
FieldDataType: fieldDataType,
|
||||||
|
|||||||
@ -62,6 +62,9 @@ func (q *builderQuery[T]) Fingerprint() string {
|
|||||||
// Add signal type
|
// Add signal type
|
||||||
parts = append(parts, fmt.Sprintf("signal=%s", q.spec.Signal.StringValue()))
|
parts = append(parts, fmt.Sprintf("signal=%s", q.spec.Signal.StringValue()))
|
||||||
|
|
||||||
|
// Add source type
|
||||||
|
parts = append(parts, fmt.Sprintf("source=%s", q.spec.Source.StringValue()))
|
||||||
|
|
||||||
// Add step interval if present
|
// Add step interval if present
|
||||||
parts = append(parts, fmt.Sprintf("step=%s", q.spec.StepInterval.String()))
|
parts = append(parts, fmt.Sprintf("step=%s", q.spec.StepInterval.String()))
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,7 @@ type querier struct {
|
|||||||
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
||||||
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
|
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
|
||||||
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
||||||
|
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
||||||
bucketCache BucketCache
|
bucketCache BucketCache
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ func New(
|
|||||||
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation],
|
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation],
|
||||||
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation],
|
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation],
|
||||||
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
|
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
|
||||||
|
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation],
|
||||||
bucketCache BucketCache,
|
bucketCache BucketCache,
|
||||||
) *querier {
|
) *querier {
|
||||||
querierSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querier")
|
querierSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querier")
|
||||||
@ -55,6 +57,7 @@ func New(
|
|||||||
traceStmtBuilder: traceStmtBuilder,
|
traceStmtBuilder: traceStmtBuilder,
|
||||||
logStmtBuilder: logStmtBuilder,
|
logStmtBuilder: logStmtBuilder,
|
||||||
metricStmtBuilder: metricStmtBuilder,
|
metricStmtBuilder: metricStmtBuilder,
|
||||||
|
meterStmtBuilder: meterStmtBuilder,
|
||||||
bucketCache: bucketCache,
|
bucketCache: bucketCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,17 +171,21 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
|||||||
event.MetricsUsed = true
|
event.MetricsUsed = true
|
||||||
event.FilterApplied = spec.Filter != nil && spec.Filter.Expression != ""
|
event.FilterApplied = spec.Filter != nil && spec.Filter.Expression != ""
|
||||||
event.GroupByApplied = len(spec.GroupBy) > 0
|
event.GroupByApplied = len(spec.GroupBy) > 0
|
||||||
if spec.StepInterval.Seconds() == 0 {
|
|
||||||
spec.StepInterval = qbtypes.Step{
|
|
||||||
Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMetric(req.Start, req.End)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepIntervalForMetric(req.Start, req.End)) {
|
|
||||||
spec.StepInterval = qbtypes.Step{
|
|
||||||
Duration: time.Second * time.Duration(querybuilder.MinAllowedStepIntervalForMetric(req.Start, req.End)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if spec.Source == telemetrytypes.SourceMeter {
|
||||||
|
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMeter(req.Start, req.End))}
|
||||||
|
} else {
|
||||||
|
if spec.StepInterval.Seconds() == 0 {
|
||||||
|
spec.StepInterval = qbtypes.Step{
|
||||||
|
Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMetric(req.Start, req.End)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepIntervalForMetric(req.Start, req.End)) {
|
||||||
|
spec.StepInterval = qbtypes.Step{
|
||||||
|
Duration: time.Second * time.Duration(querybuilder.MinAllowedStepIntervalForMetric(req.Start, req.End)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
req.CompositeQuery.Queries[idx].Spec = spec
|
req.CompositeQuery.Queries[idx].Spec = spec
|
||||||
}
|
}
|
||||||
} else if query.Type == qbtypes.QueryTypePromQL {
|
} else if query.Type == qbtypes.QueryTypePromQL {
|
||||||
@ -265,7 +272,14 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
|||||||
}
|
}
|
||||||
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
||||||
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
||||||
bq := newBuilderQuery(q.telemetryStore, q.metricStmtBuilder, spec, timeRange, req.RequestType, tmplVars)
|
var bq *builderQuery[qbtypes.MetricAggregation]
|
||||||
|
|
||||||
|
if spec.Source == telemetrytypes.SourceMeter {
|
||||||
|
bq = newBuilderQuery(q.telemetryStore, q.meterStmtBuilder, spec, timeRange, req.RequestType, tmplVars)
|
||||||
|
} else {
|
||||||
|
bq = newBuilderQuery(q.telemetryStore, q.metricStmtBuilder, spec, timeRange, req.RequestType, tmplVars)
|
||||||
|
}
|
||||||
|
|
||||||
queries[spec.Name] = bq
|
queries[spec.Name] = bq
|
||||||
steps[spec.Name] = spec.StepInterval
|
steps[spec.Name] = spec.StepInterval
|
||||||
default:
|
default:
|
||||||
@ -529,6 +543,9 @@ func (q *querier) createRangedQuery(originalQuery qbtypes.Query, timeRange qbtyp
|
|||||||
specCopy := qt.spec.Copy()
|
specCopy := qt.spec.Copy()
|
||||||
specCopy.ShiftBy = extractShiftFromBuilderQuery(specCopy)
|
specCopy.ShiftBy = extractShiftFromBuilderQuery(specCopy)
|
||||||
adjustedTimeRange := adjustTimeRangeForShift(specCopy, timeRange, qt.kind)
|
adjustedTimeRange := adjustTimeRangeForShift(specCopy, timeRange, qt.kind)
|
||||||
|
if qt.spec.Source == telemetrytypes.SourceMeter {
|
||||||
|
return newBuilderQuery(q.telemetryStore, q.meterStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables)
|
||||||
|
}
|
||||||
return newBuilderQuery(q.telemetryStore, q.metricStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables)
|
return newBuilderQuery(q.telemetryStore, q.metricStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/querybuilder/resourcefilter"
|
"github.com/SigNoz/signoz/pkg/querybuilder/resourcefilter"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrymetadata"
|
"github.com/SigNoz/signoz/pkg/telemetrymetadata"
|
||||||
|
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
||||||
@ -49,12 +50,17 @@ func newProvider(
|
|||||||
telemetryStore,
|
telemetryStore,
|
||||||
telemetrytraces.DBName,
|
telemetrytraces.DBName,
|
||||||
telemetrytraces.TagAttributesV2TableName,
|
telemetrytraces.TagAttributesV2TableName,
|
||||||
|
telemetrytraces.SpanAttributesKeysTblName,
|
||||||
telemetrytraces.SpanIndexV3TableName,
|
telemetrytraces.SpanIndexV3TableName,
|
||||||
telemetrymetrics.DBName,
|
telemetrymetrics.DBName,
|
||||||
telemetrymetrics.AttributesMetadataTableName,
|
telemetrymetrics.AttributesMetadataTableName,
|
||||||
|
telemetrymeter.DBName,
|
||||||
|
telemetrymeter.SamplesAgg1dTableName,
|
||||||
telemetrylogs.DBName,
|
telemetrylogs.DBName,
|
||||||
telemetrylogs.LogsV2TableName,
|
telemetrylogs.LogsV2TableName,
|
||||||
telemetrylogs.TagAttributesV2TableName,
|
telemetrylogs.TagAttributesV2TableName,
|
||||||
|
telemetrylogs.LogAttributeKeysTblName,
|
||||||
|
telemetrylogs.LogResourceKeysTblName,
|
||||||
telemetrymetadata.DBName,
|
telemetrymetadata.DBName,
|
||||||
telemetrymetadata.AttributesMetadataLocalTableName,
|
telemetrymetadata.AttributesMetadataLocalTableName,
|
||||||
)
|
)
|
||||||
@ -66,12 +72,13 @@ func newProvider(
|
|||||||
resourceFilterFieldMapper := resourcefilter.NewFieldMapper()
|
resourceFilterFieldMapper := resourcefilter.NewFieldMapper()
|
||||||
resourceFilterConditionBuilder := resourcefilter.NewConditionBuilder(resourceFilterFieldMapper)
|
resourceFilterConditionBuilder := resourcefilter.NewConditionBuilder(resourceFilterFieldMapper)
|
||||||
resourceFilterStmtBuilder := resourcefilter.NewTraceResourceFilterStatementBuilder(
|
resourceFilterStmtBuilder := resourcefilter.NewTraceResourceFilterStatementBuilder(
|
||||||
|
settings,
|
||||||
resourceFilterFieldMapper,
|
resourceFilterFieldMapper,
|
||||||
resourceFilterConditionBuilder,
|
resourceFilterConditionBuilder,
|
||||||
telemetryMetadataStore,
|
telemetryMetadataStore,
|
||||||
)
|
)
|
||||||
|
|
||||||
traceAggExprRewriter := querybuilder.NewAggExprRewriter(nil, traceFieldMapper, traceConditionBuilder, "", nil)
|
traceAggExprRewriter := querybuilder.NewAggExprRewriter(settings, nil, traceFieldMapper, traceConditionBuilder, "", nil)
|
||||||
traceStmtBuilder := telemetrytraces.NewTraceQueryStatementBuilder(
|
traceStmtBuilder := telemetrytraces.NewTraceQueryStatementBuilder(
|
||||||
settings,
|
settings,
|
||||||
telemetryMetadataStore,
|
telemetryMetadataStore,
|
||||||
@ -86,6 +93,7 @@ func newProvider(
|
|||||||
logFieldMapper := telemetrylogs.NewFieldMapper()
|
logFieldMapper := telemetrylogs.NewFieldMapper()
|
||||||
logConditionBuilder := telemetrylogs.NewConditionBuilder(logFieldMapper)
|
logConditionBuilder := telemetrylogs.NewConditionBuilder(logFieldMapper)
|
||||||
logResourceFilterStmtBuilder := resourcefilter.NewLogResourceFilterStatementBuilder(
|
logResourceFilterStmtBuilder := resourcefilter.NewLogResourceFilterStatementBuilder(
|
||||||
|
settings,
|
||||||
resourceFilterFieldMapper,
|
resourceFilterFieldMapper,
|
||||||
resourceFilterConditionBuilder,
|
resourceFilterConditionBuilder,
|
||||||
telemetryMetadataStore,
|
telemetryMetadataStore,
|
||||||
@ -94,6 +102,7 @@ func newProvider(
|
|||||||
telemetrylogs.GetBodyJSONKey,
|
telemetrylogs.GetBodyJSONKey,
|
||||||
)
|
)
|
||||||
logAggExprRewriter := querybuilder.NewAggExprRewriter(
|
logAggExprRewriter := querybuilder.NewAggExprRewriter(
|
||||||
|
settings,
|
||||||
telemetrylogs.DefaultFullTextColumn,
|
telemetrylogs.DefaultFullTextColumn,
|
||||||
logFieldMapper,
|
logFieldMapper,
|
||||||
logConditionBuilder,
|
logConditionBuilder,
|
||||||
@ -122,6 +131,14 @@ func newProvider(
|
|||||||
metricConditionBuilder,
|
metricConditionBuilder,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Create meter statement builder
|
||||||
|
meterStmtBuilder := telemetrymeter.NewMeterQueryStatementBuilder(
|
||||||
|
settings,
|
||||||
|
telemetryMetadataStore,
|
||||||
|
metricFieldMapper,
|
||||||
|
metricConditionBuilder,
|
||||||
|
)
|
||||||
|
|
||||||
// Create bucket cache
|
// Create bucket cache
|
||||||
bucketCache := querier.NewBucketCache(
|
bucketCache := querier.NewBucketCache(
|
||||||
settings,
|
settings,
|
||||||
@ -139,6 +156,7 @@ func newProvider(
|
|||||||
traceStmtBuilder,
|
traceStmtBuilder,
|
||||||
logStmtBuilder,
|
logStmtBuilder,
|
||||||
metricStmtBuilder,
|
metricStmtBuilder,
|
||||||
|
meterStmtBuilder,
|
||||||
bucketCache,
|
bucketCache,
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,6 +64,8 @@ const (
|
|||||||
signozTraceLocalTableName = "signoz_index_v2"
|
signozTraceLocalTableName = "signoz_index_v2"
|
||||||
signozMetricDBName = "signoz_metrics"
|
signozMetricDBName = "signoz_metrics"
|
||||||
signozMetadataDbName = "signoz_metadata"
|
signozMetadataDbName = "signoz_metadata"
|
||||||
|
signozMeterDBName = "signoz_meter"
|
||||||
|
signozMeterSamplesName = "samples_agg_1d"
|
||||||
|
|
||||||
signozSampleLocalTableName = "samples_v4"
|
signozSampleLocalTableName = "samples_v4"
|
||||||
signozSampleTableName = "distributed_samples_v4"
|
signozSampleTableName = "distributed_samples_v4"
|
||||||
@ -2741,8 +2743,55 @@ func (r *ClickHouseReader) GetMetricAggregateAttributes(ctx context.Context, org
|
|||||||
return &response, nil
|
return &response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClickHouseReader) GetMetricAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
|
func (r *ClickHouseReader) GetMeterAggregateAttributes(ctx context.Context, orgID valuer.UUID, req *v3.AggregateAttributeRequest) (*v3.AggregateAttributeResponse, error) {
|
||||||
|
var response v3.AggregateAttributeResponse
|
||||||
|
// Query all relevant metric names from time_series_v4, but leave metadata retrieval to cache/db
|
||||||
|
query := fmt.Sprintf(
|
||||||
|
`SELECT metric_name,type,temporality,is_monotonic
|
||||||
|
FROM %s.%s
|
||||||
|
WHERE metric_name ILIKE $1
|
||||||
|
GROUP BY metric_name,type,temporality,is_monotonic`,
|
||||||
|
signozMeterDBName, signozMeterSamplesName)
|
||||||
|
|
||||||
|
if req.Limit != 0 {
|
||||||
|
query = query + fmt.Sprintf(" LIMIT %d;", req.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText))
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("Error while querying meter names", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("error while executing meter name query: %s", err.Error())
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
var typ string
|
||||||
|
var temporality string
|
||||||
|
var isMonotonic bool
|
||||||
|
if err := rows.Scan(&name, &typ, &temporality, &isMonotonic); err != nil {
|
||||||
|
return nil, fmt.Errorf("error while scanning meter name: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-monotonic cumulative sums are treated as gauges
|
||||||
|
if typ == "Sum" && !isMonotonic && temporality == string(v3.Cumulative) {
|
||||||
|
typ = "Gauge"
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlike traces/logs `tag`/`resource` type, the `Type` will be metric type
|
||||||
|
key := v3.AttributeKey{
|
||||||
|
Key: name,
|
||||||
|
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||||
|
Type: v3.AttributeKeyType(typ),
|
||||||
|
IsColumn: true,
|
||||||
|
}
|
||||||
|
response.AttributeKeys = append(response.AttributeKeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClickHouseReader) GetMetricAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
|
||||||
var query string
|
var query string
|
||||||
var err error
|
var err error
|
||||||
var rows driver.Rows
|
var rows driver.Rows
|
||||||
@ -2782,6 +2831,41 @@ func (r *ClickHouseReader) GetMetricAttributeKeys(ctx context.Context, req *v3.F
|
|||||||
return &response, nil
|
return &response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ClickHouseReader) GetMeterAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
|
||||||
|
var query string
|
||||||
|
var err error
|
||||||
|
var rows driver.Rows
|
||||||
|
var response v3.FilterAttributeKeyResponse
|
||||||
|
|
||||||
|
// skips the internal attributes i.e attributes starting with __
|
||||||
|
query = fmt.Sprintf("SELECT DISTINCT arrayJoin(JSONExtractKeys(labels)) as attr_name FROM %s.%s WHERE metric_name=$1 AND attr_name ILIKE $2 AND attr_name NOT LIKE '\\_\\_%%'", signozMeterDBName, signozMeterSamplesName)
|
||||||
|
if req.Limit != 0 {
|
||||||
|
query = query + fmt.Sprintf(" LIMIT %d;", req.Limit)
|
||||||
|
}
|
||||||
|
rows, err = r.db.Query(ctx, query, req.AggregateAttribute, fmt.Sprintf("%%%s%%", req.SearchText))
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("Error while executing query", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("error while executing query: %s", err.Error())
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var attributeKey string
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Scan(&attributeKey); err != nil {
|
||||||
|
return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
|
||||||
|
}
|
||||||
|
key := v3.AttributeKey{
|
||||||
|
Key: attributeKey,
|
||||||
|
DataType: v3.AttributeKeyDataTypeString, // https://github.com/OpenObservability/OpenMetrics/blob/main/proto/openmetrics_data_model.proto#L64-L72.
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
IsColumn: false,
|
||||||
|
}
|
||||||
|
response.AttributeKeys = append(response.AttributeKeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ClickHouseReader) GetMetricAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
|
func (r *ClickHouseReader) GetMetricAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
|
||||||
|
|
||||||
var query string
|
var query string
|
||||||
|
|||||||
@ -4218,6 +4218,8 @@ func (aH *APIHandler) autocompleteAggregateAttributes(w http.ResponseWriter, r *
|
|||||||
response, err = aH.reader.GetLogAggregateAttributes(r.Context(), req)
|
response, err = aH.reader.GetLogAggregateAttributes(r.Context(), req)
|
||||||
case v3.DataSourceTraces:
|
case v3.DataSourceTraces:
|
||||||
response, err = aH.reader.GetTraceAggregateAttributes(r.Context(), req)
|
response, err = aH.reader.GetTraceAggregateAttributes(r.Context(), req)
|
||||||
|
case v3.DataSourceMeter:
|
||||||
|
response, err = aH.reader.GetMeterAggregateAttributes(r.Context(), orgID, req)
|
||||||
default:
|
default:
|
||||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid data source")}, nil)
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid data source")}, nil)
|
||||||
return
|
return
|
||||||
@ -4267,6 +4269,8 @@ func (aH *APIHandler) autoCompleteAttributeKeys(w http.ResponseWriter, r *http.R
|
|||||||
switch req.DataSource {
|
switch req.DataSource {
|
||||||
case v3.DataSourceMetrics:
|
case v3.DataSourceMetrics:
|
||||||
response, err = aH.reader.GetMetricAttributeKeys(r.Context(), req)
|
response, err = aH.reader.GetMetricAttributeKeys(r.Context(), req)
|
||||||
|
case v3.DataSourceMeter:
|
||||||
|
response, err = aH.reader.GetMeterAttributeKeys(r.Context(), req)
|
||||||
case v3.DataSourceLogs:
|
case v3.DataSourceLogs:
|
||||||
response, err = aH.reader.GetLogAttributeKeys(r.Context(), req)
|
response, err = aH.reader.GetLogAttributeKeys(r.Context(), req)
|
||||||
case v3.DataSourceTraces:
|
case v3.DataSourceTraces:
|
||||||
|
|||||||
@ -484,7 +484,7 @@ func parseAggregateAttributeRequest(r *http.Request) (*v3.AggregateAttributeRequ
|
|||||||
limit = 50
|
limit = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataSource != v3.DataSourceMetrics {
|
if dataSource != v3.DataSourceMetrics && dataSource != v3.DataSourceMeter {
|
||||||
if err := aggregateOperator.Validate(); err != nil {
|
if err := aggregateOperator.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -604,7 +604,7 @@ func parseFilterAttributeKeyRequest(r *http.Request) (*v3.FilterAttributeKeyRequ
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataSource != v3.DataSourceMetrics {
|
if dataSource != v3.DataSourceMetrics && dataSource != v3.DataSourceMeter {
|
||||||
if err := aggregateOperator.Validate(); err != nil {
|
if err := aggregateOperator.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,9 @@ type Reader interface {
|
|||||||
|
|
||||||
FetchTemporality(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]map[v3.Temporality]bool, error)
|
FetchTemporality(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]map[v3.Temporality]bool, error)
|
||||||
GetMetricAggregateAttributes(ctx context.Context, orgID valuer.UUID, req *v3.AggregateAttributeRequest, skipSignozMetrics bool) (*v3.AggregateAttributeResponse, error)
|
GetMetricAggregateAttributes(ctx context.Context, orgID valuer.UUID, req *v3.AggregateAttributeRequest, skipSignozMetrics bool) (*v3.AggregateAttributeResponse, error)
|
||||||
|
GetMeterAggregateAttributes(ctx context.Context, orgID valuer.UUID, req *v3.AggregateAttributeRequest) (*v3.AggregateAttributeResponse, error)
|
||||||
GetMetricAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error)
|
GetMetricAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error)
|
||||||
|
GetMeterAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error)
|
||||||
GetMetricAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error)
|
GetMetricAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error)
|
||||||
|
|
||||||
// Returns `MetricStatus` for latest received metric among `metricNames`. Useful for status calculations
|
// Returns `MetricStatus` for latest received metric among `metricNames`. Useful for status calculations
|
||||||
|
|||||||
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