mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 02:19:02 +08:00
commit
78d2377520
2
.github/workflows/e2e-k3s.yaml
vendored
2
.github/workflows/e2e-k3s.yaml
vendored
@ -16,6 +16,8 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build query-service image
|
||||
env:
|
||||
DEV_BUILD: 1
|
||||
run: make build-ee-query-service-amd64
|
||||
|
||||
- name: Build frontend image
|
||||
|
8
Makefile
8
Makefile
@ -120,14 +120,10 @@ down-local:
|
||||
down -v
|
||||
|
||||
run-x86:
|
||||
@docker-compose -f \
|
||||
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \
|
||||
up --build -d
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up --build -d
|
||||
|
||||
down-x86:
|
||||
@docker-compose -f \
|
||||
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \
|
||||
down -v
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v
|
||||
|
||||
clear-standalone-data:
|
||||
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
|
||||
|
@ -78,6 +78,12 @@ We support [OpenTelemetry](https://opentelemetry.io) as the library which you ca
|
||||
- Python
|
||||
- NodeJS
|
||||
- Go
|
||||
- PHP
|
||||
- .NET
|
||||
- Ruby
|
||||
- Elixir
|
||||
- Rust
|
||||
|
||||
|
||||
You can find the complete list of languages here - https://opentelemetry.io/docs/
|
||||
|
||||
|
@ -13,14 +13,19 @@
|
||||
|
||||
##
|
||||
|
||||
SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式跟踪来增加软件技术栈的可见性。
|
||||
SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式追踪来增加软件技术栈的可见性。
|
||||
|
||||
👉 你能看到一些性能矩阵,服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。
|
||||
👉 你能看到一些性能指标,服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。
|
||||
|
||||
👉 通过准确的跟踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。
|
||||
👉 通过准确的追踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。
|
||||
|
||||
👉 聚合trace数据来获得业务相关指标。
|
||||
|
||||

|
||||

|
||||
<br />
|
||||

|
||||
<br />
|
||||

|
||||
|
||||
<br /><br />
|
||||
|
||||
@ -36,12 +41,12 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo
|
||||
|
||||
## 功能:
|
||||
|
||||
- 应用总览矩阵(matrix),如RPS, 50/90/99百分比延迟率,错误率
|
||||
- 应用概览指标(metrics),如RPS, p50/p90/p99延迟率分位值,错误率等。
|
||||
- 应用中最慢的终端(endpoint)
|
||||
- 查看准确的网络请求跟踪来分析下游服务问题、慢数据库查询问题 及调用第三方服务如支付网关的问题
|
||||
- 通过服务名称、操作、延迟、错误、标签来过滤跟踪
|
||||
- 对过滤后的跟踪数据做矩阵聚合。比如,获得过滤条件`customer_type: gold` or `deployment_version: v2` or `external_call: paypal`的错误率和p99延迟
|
||||
- 整合的矩阵和跟踪用户界面。不需要像从Prometheus切换到Jaeger才能调试问题
|
||||
- 查看特定请求的trace数据来分析下游服务问题、慢数据库查询问题 及调用第三方服务如支付网关的问题
|
||||
- 通过服务名称、操作、延迟、错误、标签来过滤traces。
|
||||
- 聚合trace数据(events/spans)来得到业务相关指标。比如,你可以通过过滤条件`customer_type: gold` or `deployment_version: v2` or `external_call: paypal` 来获取指定业务的错误率和p99延迟
|
||||
- 为metrics和trace提供统一的UI。排查问题不需要在Prometheus和Jaeger之间切换。
|
||||
|
||||
<br /><br />
|
||||
|
||||
@ -53,7 +58,7 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo
|
||||
|
||||
我们想做一个自服务的开源版本的工具,类似于DataDog和NewRelic,用于那些对客户数据流入第三方有隐私和安全担忧的厂商。
|
||||
|
||||
开源也让你对配置、采样和上线率有完整的控制,你可以在SigNoz基础上构建模块来满足特定的商业需求。
|
||||
开源也让你对配置、采样和正常运行时间有完整的控制,你可以在SigNoz基础上构建模块来满足特定的商业需求。
|
||||
|
||||
### 语言支持
|
||||
|
||||
@ -71,8 +76,8 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Philosophy.svg" width="50px" />
|
||||
|
||||
## 入门
|
||||
|
||||
|
||||
|
||||
|
||||
### 使用Docker部署
|
||||
|
||||
请按照[这里](https://signoz.io/docs/deployment/docker/)列出的步骤使用Docker来安装
|
||||
@ -80,35 +85,34 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo
|
||||
如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/deployment/troubleshooting)会对你有帮助。
|
||||
|
||||
<p>  </p>
|
||||
|
||||
|
||||
|
||||
|
||||
### 使用Helm在Kubernetes上部署
|
||||
|
||||
请跟着[这里](https://signoz.io/docs/deployment/helm_chart)的步骤使用helm charts安装
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/UseSigNoz.svg" width="50px" />
|
||||
|
||||
## Comparisons to Familiar Tools
|
||||
## 与其他方案的比较
|
||||
|
||||
### SigNoz vs Prometheus
|
||||
|
||||
如果你只是需要矩阵,那Prometheus是不错的,但如果你要无缝的在矩阵和跟踪之间切换,那目前把Prometheus & Jaeger串起来的体验并不好。
|
||||
如果你只是需要监控指标(metrics),那Prometheus是不错的,但如果你要无缝的在metrics和traces之间切换,那目前把Prometheus & Jaeger串起来的体验并不好。
|
||||
|
||||
我们的目标是在矩阵和跟踪之间提供整合的UI - 类似于Datadog这样的Saas厂提供的方案,能够对跟踪进行过滤和聚合,这是目前Jaeger缺失的功能。
|
||||
我们的目标是为metrics和traces提供统一的UI - 类似于Datadog这样的Saas厂提供的方案。并且能够对trace进行过滤和聚合,这是目前Jaeger缺失的功能。
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Jaeger
|
||||
|
||||
Jaeger只做分布式跟踪,SigNoz则是做了矩阵和跟踪两块,我们在计划中也有日志管理功能。
|
||||
Jaeger只做分布式追踪(distributed tracing),SigNoz则支持metrics,traces,logs ,即可视化的三大支柱。
|
||||
|
||||
并且SigNoz有一些Jaeger没有的高级功能:
|
||||
|
||||
- Jaegar UI无法在跟踪或过滤的跟踪基础上展示矩阵。
|
||||
- Jaeger不能在过滤的跟踪上进行聚合操作。例如,拥有tag为customer_type='premium'的所有请求的p99延迟,在SigNoz里这很容易实现。
|
||||
- Jaegar UI无法在traces或过滤的traces上展示metrics。
|
||||
- Jaeger不能对过滤的traces做聚合操作。例如,拥有tag为customer_type='premium'的所有请求的p99延迟。而这个功能在SigNoz这儿是很容易实现。
|
||||
|
||||
<br /><br />
|
||||
|
||||
@ -121,6 +125,23 @@ Jaeger只做分布式跟踪,SigNoz则是做了矩阵和跟踪两块,我们
|
||||
|
||||
还不清楚怎么开始? 只需在[slack社区](https://signoz.io/slack)的`#contributing`频道里ping我们。
|
||||
|
||||
### Project maintainers
|
||||
|
||||
#### Backend
|
||||
|
||||
- [Ankit Nayan](https://github.com/ankitnayan)
|
||||
- [Nityananda Gohain](https://github.com/nityanandagohain)
|
||||
- [Srikanth Chekuri](https://github.com/srikanthccv)
|
||||
- [Vishal Sharma](https://github.com/makeavish)
|
||||
|
||||
#### Frontend
|
||||
|
||||
- [Palash Gupta](https://github.com/palashgdev)
|
||||
|
||||
#### DevOps
|
||||
|
||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/DevelopingLocally.svg" width="50px" />
|
||||
|
@ -2,7 +2,7 @@ version: "3.9"
|
||||
|
||||
services:
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:22.4.5-alpine
|
||||
image: clickhouse/clickhouse-server:22.8.8-alpine
|
||||
# ports:
|
||||
# - "9000:9000"
|
||||
# - "8123:8123"
|
||||
@ -40,7 +40,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.11.2
|
||||
image: signoz/query-service:0.11.3
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@ -70,7 +70,7 @@ services:
|
||||
- clickhouse
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.11.2
|
||||
image: signoz/frontend:0.11.3
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@ -83,7 +83,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.55.3
|
||||
image: signoz/signoz-otel-collector:0.63.0
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -111,7 +111,7 @@ services:
|
||||
- clickhouse
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.55.3
|
||||
image: signoz/signoz-otel-collector:0.63.0
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -47,7 +47,7 @@ receivers:
|
||||
# thrift_binary:
|
||||
# endpoint: 0.0.0.0:6832
|
||||
hostmetrics:
|
||||
collection_interval: 60s
|
||||
collection_interval: 30s
|
||||
scrapers:
|
||||
cpu: {}
|
||||
load: {}
|
||||
@ -55,6 +55,16 @@ receivers:
|
||||
disk: {}
|
||||
filesystem: {}
|
||||
network: {}
|
||||
prometheus:
|
||||
config:
|
||||
global:
|
||||
scrape_interval: 60s
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: otel-collector
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
|
||||
processors:
|
||||
batch:
|
||||
@ -65,7 +75,6 @@ processors:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
override: false
|
||||
signozspanmetrics/prometheus:
|
||||
metrics_exporter: prometheus
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
@ -134,8 +143,8 @@ service:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/hostmetrics:
|
||||
receivers: [hostmetrics]
|
||||
metrics/generic:
|
||||
receivers: [hostmetrics, prometheus]
|
||||
processors: [resourcedetection, batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/spanmetrics:
|
||||
|
@ -2,27 +2,19 @@ receivers:
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: "otel-collector"
|
||||
scrape_interval: 60s
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'tasks.otel-collector'
|
||||
type: 'A'
|
||||
port: 8888
|
||||
# otel-collector-metrics internal metrics
|
||||
- job_name: "otel-collector-metrics"
|
||||
- job_name: otel-collector-metrics
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
# SigNoz span metrics
|
||||
- job_name: "signozspanmetrics-collector"
|
||||
- job_name: signozspanmetrics-collector
|
||||
scrape_interval: 60s
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'tasks.otel-collector'
|
||||
type: 'A'
|
||||
- tasks.otel-collector
|
||||
type: A
|
||||
port: 8889
|
||||
|
||||
processors:
|
||||
|
@ -2,7 +2,7 @@ version: "2.4"
|
||||
|
||||
services:
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:22.4.5-alpine
|
||||
image: clickhouse/clickhouse-server:22.8.8-alpine
|
||||
container_name: clickhouse
|
||||
# ports:
|
||||
# - "9000:9000"
|
||||
@ -41,7 +41,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
otel-collector:
|
||||
container_name: otel-collector
|
||||
image: signoz/signoz-otel-collector:0.55.3
|
||||
image: signoz/signoz-otel-collector:0.63.0
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
# user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -67,7 +67,7 @@ services:
|
||||
|
||||
otel-collector-metrics:
|
||||
container_name: otel-collector-metrics
|
||||
image: signoz/signoz-otel-collector:0.55.3
|
||||
image: signoz/signoz-otel-collector:0.63.0
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -1,44 +0,0 @@
|
||||
version: "2.4"
|
||||
|
||||
services:
|
||||
query-service:
|
||||
image: signoz/query-service:0.11.2
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
# - "8080:8080" # query-service port
|
||||
volumes:
|
||||
- ./prometheus.yml:/root/config/prometheus.yml
|
||||
- ../dashboards:/root/config/dashboards
|
||||
- ./data/signoz/:/var/lib/signoz/
|
||||
environment:
|
||||
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
|
||||
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
|
||||
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
|
||||
- DASHBOARDS_PATH=/root/config/dashboards
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.11.2
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
- alertmanager
|
||||
- query-service
|
||||
ports:
|
||||
- "3301:3301"
|
||||
volumes:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
@ -2,7 +2,7 @@ version: "2.4"
|
||||
|
||||
services:
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:22.4.5-alpine
|
||||
image: clickhouse/clickhouse-server:22.8.8-alpine
|
||||
# ports:
|
||||
# - "9000:9000"
|
||||
# - "8123:8123"
|
||||
@ -39,7 +39,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.11.2
|
||||
image: signoz/query-service:0.11.3
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
@ -69,7 +69,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.11.2
|
||||
image: signoz/frontend:0.11.3
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@ -81,7 +81,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.55.3
|
||||
image: signoz/signoz-otel-collector:0.63.0
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -107,7 +107,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.55.3
|
||||
image: signoz/signoz-otel-collector:0.63.0
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -47,7 +47,7 @@ receivers:
|
||||
# thrift_binary:
|
||||
# endpoint: 0.0.0.0:6832
|
||||
hostmetrics:
|
||||
collection_interval: 60s
|
||||
collection_interval: 30s
|
||||
scrapers:
|
||||
cpu: {}
|
||||
load: {}
|
||||
@ -55,6 +55,16 @@ receivers:
|
||||
disk: {}
|
||||
filesystem: {}
|
||||
network: {}
|
||||
prometheus:
|
||||
config:
|
||||
global:
|
||||
scrape_interval: 60s
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: otel-collector
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
|
||||
processors:
|
||||
batch:
|
||||
@ -89,7 +99,6 @@ processors:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
override: false
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
@ -138,8 +147,8 @@ service:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/hostmetrics:
|
||||
receivers: [hostmetrics]
|
||||
metrics/generic:
|
||||
receivers: [hostmetrics, prometheus]
|
||||
processors: [resourcedetection, batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/spanmetrics:
|
||||
|
@ -6,20 +6,14 @@ receivers:
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: "otel-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- otel-collector:8888
|
||||
# otel-collector-metrics internal metrics
|
||||
- job_name: "otel-collector-metrics"
|
||||
- job_name: otel-collector-metrics
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
# SigNoz span metrics
|
||||
- job_name: "signozspanmetrics-collector"
|
||||
- job_name: signozspanmetrics-collector
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
|
||||
"sync"
|
||||
|
||||
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
|
||||
|
||||
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
@ -92,6 +94,8 @@ func (lm *Manager) SetActive(l *model.License) {
|
||||
|
||||
lm.activeLicense = l
|
||||
lm.activeFeatures = l.FeatureSet
|
||||
// set default features
|
||||
setDefaultFeatures(lm)
|
||||
if !lm.validatorRunning {
|
||||
// we want to make sure only one validator runs,
|
||||
// we already have lock() so good to go
|
||||
@ -101,7 +105,13 @@ func (lm *Manager) SetActive(l *model.License) {
|
||||
|
||||
}
|
||||
|
||||
// LoadActiveLicense loads the most recent active licenseex
|
||||
func setDefaultFeatures(lm *Manager) {
|
||||
for k, v := range baseconstants.DEFAULT_FEATURE_SET {
|
||||
lm.activeFeatures[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// LoadActiveLicense loads the most recent active license
|
||||
func (lm *Manager) LoadActiveLicense() error {
|
||||
var err error
|
||||
active, err := lm.repo.GetActiveLicense(context.Background())
|
||||
@ -111,7 +121,10 @@ func (lm *Manager) LoadActiveLicense() error {
|
||||
if active != nil {
|
||||
lm.SetActive(active)
|
||||
} else {
|
||||
zap.S().Info("No active license found.")
|
||||
zap.S().Info("No active license found, defaulting to basic plan")
|
||||
// if no active license is found, we default to basic(free) plan with all default features
|
||||
lm.activeFeatures = basemodel.BasicPlan
|
||||
setDefaultFeatures(lm)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -278,8 +291,11 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
|
||||
// CheckFeature will be internally used by backend routines
|
||||
// for feature gating
|
||||
func (lm *Manager) CheckFeature(featureKey string) error {
|
||||
if _, ok := lm.activeFeatures[featureKey]; ok {
|
||||
return nil
|
||||
if value, ok := lm.activeFeatures[featureKey]; ok {
|
||||
if value {
|
||||
return nil
|
||||
}
|
||||
return basemodel.ErrFeatureUnavailable{Key: featureKey}
|
||||
}
|
||||
return basemodel.ErrFeatureUnavailable{Key: featureKey}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"create_dashboard": "Create Dashboard",
|
||||
"import_json": "Import JSON",
|
||||
"import_grafana_json": "Import Grafana JSON",
|
||||
"copy_to_clipboard": "Copy To ClipBoard",
|
||||
"download_json": "Download JSON",
|
||||
"view_json": "View JSON",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"create_dashboard": "Create Dashboard",
|
||||
"import_json": "Import JSON",
|
||||
"import_grafana_json": "Import Grafana JSON",
|
||||
"copy_to_clipboard": "Copy To ClipBoard",
|
||||
"download_json": "Download JSON",
|
||||
"view_json": "View JSON",
|
||||
|
@ -7,8 +7,9 @@ import { PayloadProps, Props } from 'types/api/dashboard/create';
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const url = props.uploadedGrafana ? '/dashboards/grafana' : '/dashboards';
|
||||
try {
|
||||
const response = await axios.post('/dashboards', {
|
||||
const response = await axios.post(url, {
|
||||
...props,
|
||||
});
|
||||
|
||||
|
24
frontend/src/api/dynamicConfigs/getDynamicConfigs.ts
Normal file
24
frontend/src/api/dynamicConfigs/getDynamicConfigs.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||
|
||||
const getDynamicConfigs = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get(`/configs`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getDynamicConfigs;
|
@ -11,3 +11,5 @@ export const INVITE_MEMBERS_HASH = '#invite-team-members';
|
||||
|
||||
export const SIGNOZ_UPGRADE_PLAN_URL =
|
||||
'https://upgrade.signoz.io/upgrade-from-app';
|
||||
|
||||
export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { notification } from 'antd';
|
||||
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
||||
import getFeaturesFlags from 'api/features/getFeatureFlags';
|
||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||
import getUserVersion from 'api/user/getVersion';
|
||||
@ -14,6 +15,7 @@ import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
UPDATE_CONFIGS,
|
||||
UPDATE_CURRENT_ERROR,
|
||||
UPDATE_CURRENT_VERSION,
|
||||
UPDATE_FEATURE_FLAGS,
|
||||
@ -33,6 +35,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
getUserVersionResponse,
|
||||
getUserLatestVersionResponse,
|
||||
getFeaturesResponse,
|
||||
getDynamicConfigsResponse,
|
||||
] = useQueries([
|
||||
{
|
||||
queryFn: getUserVersion,
|
||||
@ -48,6 +51,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
queryFn: getFeaturesFlags,
|
||||
queryKey: 'getFeatureFlags',
|
||||
},
|
||||
{
|
||||
queryFn: getDynamicConfigs,
|
||||
queryKey: 'getDynamicConfigs',
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -65,11 +72,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
if (getFeaturesResponse.status === 'idle') {
|
||||
getFeaturesResponse.refetch();
|
||||
}
|
||||
if (getDynamicConfigsResponse.status === 'idle') {
|
||||
getDynamicConfigsResponse.refetch();
|
||||
}
|
||||
}, [
|
||||
getFeaturesResponse,
|
||||
getUserLatestVersionResponse,
|
||||
getUserVersionResponse,
|
||||
isLoggedIn,
|
||||
getDynamicConfigsResponse,
|
||||
]);
|
||||
|
||||
const { children } = props;
|
||||
@ -78,6 +89,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const latestCurrentCounter = useRef(0);
|
||||
const latestVersionCounter = useRef(0);
|
||||
const latestConfigCounter = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -170,6 +182,23 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
getDynamicConfigsResponse.isFetched &&
|
||||
getDynamicConfigsResponse.isSuccess &&
|
||||
getDynamicConfigsResponse.data &&
|
||||
getDynamicConfigsResponse.data.payload &&
|
||||
latestConfigCounter.current === 0
|
||||
) {
|
||||
latestConfigCounter.current = 1;
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_CONFIGS,
|
||||
payload: {
|
||||
configs: getDynamicConfigsResponse.data.payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
isLoggedIn,
|
||||
@ -187,6 +216,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
getFeaturesResponse.isFetched,
|
||||
getFeaturesResponse.isSuccess,
|
||||
getFeaturesResponse.data,
|
||||
getDynamicConfigsResponse.data,
|
||||
getDynamicConfigsResponse.isFetched,
|
||||
getDynamicConfigsResponse.isSuccess,
|
||||
]);
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
|
33
frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx
Normal file
33
frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
class ErrorLink extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(): State {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { children } = this.props;
|
||||
const { hasError } = this.state;
|
||||
|
||||
if (hasError) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorLink;
|
23
frontend/src/container/ConfigDropdown/Config/Link.tsx
Normal file
23
frontend/src/container/ConfigDropdown/Config/Link.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function LinkContainer({ children, href }: LinkContainerProps): JSX.Element {
|
||||
const isInternalLink = href.startsWith('/');
|
||||
|
||||
if (isInternalLink) {
|
||||
return <Link to={href}>{children}</Link>;
|
||||
}
|
||||
|
||||
return (
|
||||
<a rel="noreferrer" target="_blank" href={href}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
interface LinkContainerProps {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export default LinkContainer;
|
51
frontend/src/container/ConfigDropdown/Config/index.tsx
Normal file
51
frontend/src/container/ConfigDropdown/Config/index.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Menu, Space } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import React, { Suspense, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import ErrorLink from './ErrorLink';
|
||||
import LinkContainer from './Link';
|
||||
|
||||
function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
||||
const sortedConfig = useMemo(
|
||||
() => config.components.sort((a, b) => a.position - b.position),
|
||||
[config.components],
|
||||
);
|
||||
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
return (
|
||||
<Menu.ItemGroup>
|
||||
{sortedConfig.map((item) => {
|
||||
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
|
||||
|
||||
const Component = React.lazy(
|
||||
() => import(`@ant-design/icons/es/icons/${iconName}.js`),
|
||||
);
|
||||
return (
|
||||
<ErrorLink key={item.text + item.href}>
|
||||
<Suspense fallback={<Spinner height="5vh" />}>
|
||||
<Menu.Item>
|
||||
<LinkContainer href={item.href}>
|
||||
<Space size="small" align="start">
|
||||
<Component />
|
||||
{item.text}
|
||||
</Space>
|
||||
</LinkContainer>
|
||||
</Menu.Item>
|
||||
</Suspense>
|
||||
</ErrorLink>
|
||||
);
|
||||
})}
|
||||
</Menu.ItemGroup>
|
||||
);
|
||||
}
|
||||
|
||||
interface HelpToolTipProps {
|
||||
config: ConfigProps;
|
||||
}
|
||||
|
||||
export default HelpToolTip;
|
67
frontend/src/container/ConfigDropdown/index.tsx
Normal file
67
frontend/src/container/ConfigDropdown/index.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import {
|
||||
CaretDownFilled,
|
||||
CaretUpFilled,
|
||||
QuestionCircleFilled,
|
||||
QuestionCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import HelpToolTip from './Config';
|
||||
|
||||
function DynamicConfigDropdown({
|
||||
frontendId,
|
||||
}: DynamicConfigDropdownProps): JSX.Element {
|
||||
const { configs, isDarkMode } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState<boolean>(false);
|
||||
|
||||
const config = useMemo(
|
||||
() =>
|
||||
Object.values(configs).find(
|
||||
(config) => config.frontendPositionId === frontendId,
|
||||
),
|
||||
[frontendId, configs],
|
||||
);
|
||||
|
||||
const onToggleHandler = (): void => {
|
||||
setIsHelpDropDownOpen(!isHelpDropDownOpen);
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const Icon = isDarkMode ? QuestionCircleOutlined : QuestionCircleFilled;
|
||||
const DropDownIcon = isHelpDropDownOpen ? CaretUpFilled : CaretDownFilled;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
onVisibleChange={onToggleHandler}
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<Menu>
|
||||
<HelpToolTip config={config} />
|
||||
</Menu>
|
||||
}
|
||||
visible={isHelpDropDownOpen}
|
||||
>
|
||||
<Space align="center">
|
||||
<Icon
|
||||
style={{ fontSize: 26, color: 'white', paddingTop: 26, cursor: 'pointer' }}
|
||||
/>
|
||||
<DropDownIcon style={{ color: 'white' }} />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
interface DynamicConfigDropdownProps {
|
||||
frontendId: string;
|
||||
}
|
||||
|
||||
export default DynamicConfigDropdown;
|
@ -14,8 +14,9 @@ import {
|
||||
} from 'antd';
|
||||
import { Logout } from 'api/utils';
|
||||
import ROUTES from 'constants/routes';
|
||||
import Config from 'container/ConfigDropdown';
|
||||
import setTheme, { AppMode } from 'lib/theme/setTheme';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { bindActionCreators } from 'redux';
|
||||
@ -28,13 +29,19 @@ import AppReducer from 'types/reducer/app';
|
||||
import CurrentOrganization from './CurrentOrganization';
|
||||
import ManageLicense from './ManageLicense';
|
||||
import SignedInAS from './SignedInAs';
|
||||
import { Container, LogoutContainer, ToggleButton } from './styles';
|
||||
import {
|
||||
Container,
|
||||
IconContainer,
|
||||
LogoutContainer,
|
||||
ToggleButton,
|
||||
} from './styles';
|
||||
|
||||
function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
||||
const { isDarkMode, user, currentVersion } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>();
|
||||
|
||||
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
|
||||
|
||||
const onToggleThemeHandler = useCallback(() => {
|
||||
const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode';
|
||||
@ -57,22 +64,21 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
||||
};
|
||||
}, [toggleDarkMode, isDarkMode]);
|
||||
|
||||
const onArrowClickHandler: VoidFunction = () => {
|
||||
setIsUserDropDownOpen((state) => !state);
|
||||
};
|
||||
|
||||
const onClickLogoutHandler = (): void => {
|
||||
Logout();
|
||||
};
|
||||
const onToggleHandler = useCallback(
|
||||
(functionToExecute: Dispatch<SetStateAction<boolean>>) => (): void => {
|
||||
functionToExecute((state) => !state);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const menu = (
|
||||
<Menu style={{ padding: '1rem' }}>
|
||||
<Menu.ItemGroup>
|
||||
<SignedInAS />
|
||||
<Divider />
|
||||
<CurrentOrganization onToggle={onArrowClickHandler} />
|
||||
<CurrentOrganization onToggle={onToggleHandler(setIsUserDropDownOpen)} />
|
||||
<Divider />
|
||||
<ManageLicense onToggle={onArrowClickHandler} />
|
||||
<ManageLicense onToggle={onToggleHandler(setIsUserDropDownOpen)} />
|
||||
<Divider />
|
||||
<LogoutContainer>
|
||||
<LogoutOutlined />
|
||||
@ -80,11 +86,11 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
||||
tabIndex={0}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === 'Space') {
|
||||
onClickLogoutHandler();
|
||||
Logout();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
onClick={onClickLogoutHandler}
|
||||
onClick={Logout}
|
||||
>
|
||||
<Typography.Link>Logout</Typography.Link>
|
||||
</div>
|
||||
@ -94,23 +100,20 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout.Header
|
||||
style={{
|
||||
paddingLeft: '1.125rem',
|
||||
paddingRight: '1.125rem',
|
||||
}}
|
||||
>
|
||||
<Layout.Header>
|
||||
<Container>
|
||||
<NavLink
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
||||
to={ROUTES.APPLICATION}
|
||||
>
|
||||
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
|
||||
<Typography.Title style={{ margin: 0, color: '#DBDBDB' }} level={4}>
|
||||
SigNoz
|
||||
</Typography.Title>
|
||||
<NavLink to={ROUTES.APPLICATION}>
|
||||
<Space align="center" direction="horizontal">
|
||||
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
|
||||
<Typography.Title style={{ margin: 0, color: '#DBDBDB' }} level={4}>
|
||||
SigNoz
|
||||
</Typography.Title>
|
||||
</Space>
|
||||
</NavLink>
|
||||
<Space align="center">
|
||||
|
||||
<Space style={{ height: '100%' }} align="center">
|
||||
<Config frontendId="tooltip" />
|
||||
|
||||
<ToggleButton
|
||||
checked={isDarkMode}
|
||||
onChange={onToggleThemeHandler}
|
||||
@ -120,26 +123,16 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
onVisibleChange={onArrowClickHandler}
|
||||
onVisibleChange={onToggleHandler(setIsUserDropDownOpen)}
|
||||
trigger={['click']}
|
||||
overlay={menu}
|
||||
visible={isUserDropDownOpen}
|
||||
>
|
||||
<Space>
|
||||
<Avatar shape="circle">{user?.name[0]}</Avatar>
|
||||
{!isUserDropDownOpen ? (
|
||||
<CaretDownFilled
|
||||
style={{
|
||||
color: '#DBDBDB',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CaretUpFilled
|
||||
style={{
|
||||
color: '#DBDBDB',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconContainer>
|
||||
{!isUserDropDownOpen ? <CaretDownFilled /> : <CaretUpFilled />}
|
||||
</IconContainer>
|
||||
</Space>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
|
@ -4,6 +4,7 @@ import styled from 'styled-components';
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const AvatarContainer = styled.div`
|
||||
@ -61,3 +62,7 @@ export const ToggleButton = styled(Switch)<DarkModeProps>`
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div`
|
||||
color: white;
|
||||
`;
|
||||
|
@ -26,6 +26,7 @@ import { EditorContainer, FooterContainer } from './styles';
|
||||
|
||||
function ImportJSON({
|
||||
isImportJSONModalVisible,
|
||||
uploadedGrafana,
|
||||
onModalHandler,
|
||||
}: ImportJSONProps): JSX.Element {
|
||||
const [jsonData, setJsonData] = useState<Record<string, unknown>>();
|
||||
@ -89,6 +90,7 @@ function ImportJSON({
|
||||
|
||||
const response = await createDashboard({
|
||||
...parsedWidgets,
|
||||
uploadedGrafana,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
@ -186,6 +188,7 @@ function ImportJSON({
|
||||
interface ImportJSONProps {
|
||||
isImportJSONModalVisible: boolean;
|
||||
onModalHandler: VoidFunction;
|
||||
uploadedGrafana: boolean;
|
||||
}
|
||||
|
||||
export default ImportJSON;
|
||||
|
@ -57,6 +57,7 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
isImportJSONModalVisible,
|
||||
setIsImportJSONModalVisible,
|
||||
] = useState<boolean>(false);
|
||||
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
|
||||
|
||||
const [filteredDashboards, setFilteredDashboards] = useState<Dashboard[]>();
|
||||
|
||||
@ -137,6 +138,7 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
title: t('new_dashboard_title', {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
@ -182,8 +184,9 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
newDashboardState.loading,
|
||||
]);
|
||||
|
||||
const onModalHandler = (): void => {
|
||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||
setIsImportJSONModalVisible((state) => !state);
|
||||
setUploadedGrafana(uploadedGrafana);
|
||||
};
|
||||
|
||||
const menu = useMemo(
|
||||
@ -198,9 +201,18 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
{t('create_dashboard')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item onClick={onModalHandler} key={t('import_json').toString()}>
|
||||
<Menu.Item
|
||||
onClick={(): void => onModalHandler(false)}
|
||||
key={t('import_json').toString()}
|
||||
>
|
||||
{t('import_json')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={(): void => onModalHandler(true)}
|
||||
key={t('import_grafana_json').toString()}
|
||||
>
|
||||
{t('import_grafana_json')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
),
|
||||
[createNewDashboard, loading, onNewDashboardHandler, t],
|
||||
@ -256,7 +268,8 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
<TableContainer>
|
||||
<ImportJSON
|
||||
isImportJSONModalVisible={isImportJSONModalVisible}
|
||||
onModalHandler={onModalHandler}
|
||||
uploadedGrafana={uploadedGrafana}
|
||||
onModalHandler={(): void => onModalHandler(false)}
|
||||
/>
|
||||
<Table
|
||||
pagination={{
|
||||
|
@ -6,24 +6,17 @@ import LogsAggregate from 'container/LogsAggregate';
|
||||
import LogsFilters from 'container/LogsFilters';
|
||||
import SearchFilter from 'container/LogsSearchFilter';
|
||||
import LogsTable from 'container/LogsTable';
|
||||
import React, { memo, useEffect, useMemo } from 'react';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { GetLogsFields } from 'store/actions/logs/getFields';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
|
||||
|
||||
interface LogsProps {
|
||||
getLogsFields: VoidFunction;
|
||||
}
|
||||
function Logs({ getLogsFields }: LogsProps): JSX.Element {
|
||||
const { search } = useLocation();
|
||||
|
||||
const urlQuery = useMemo(() => {
|
||||
return new URLSearchParams(search);
|
||||
}, [search]);
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@ -58,6 +51,8 @@ function Logs({ getLogsFields }: LogsProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
type LogsProps = DispatchProps;
|
||||
|
||||
interface DispatchProps {
|
||||
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
@ -22,10 +22,11 @@ import { v4 } from 'uuid';
|
||||
import FieldKey from '../FieldKey';
|
||||
import { QueryConditionContainer, QueryFieldContainer } from '../styles';
|
||||
import { createParsedQueryStructure } from '../utils';
|
||||
import { hashCode, parseQuery } from './utils';
|
||||
|
||||
const { Option } = Select;
|
||||
interface QueryFieldProps {
|
||||
query: { value: string | string[]; type: string }[];
|
||||
query: Query;
|
||||
queryIndex: number;
|
||||
onUpdate: (query: unknown, queryIndex: number) => void;
|
||||
onDelete: (queryIndex: number) => void;
|
||||
@ -49,12 +50,12 @@ function QueryField({
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const fieldType = useMemo(() => getFieldType(query[0].value as string), [
|
||||
query,
|
||||
]);
|
||||
const handleChange = (qIdx: number, value: string): void => {
|
||||
query[qIdx].value = value || '';
|
||||
|
||||
if (qIdx === 1) {
|
||||
if (Object.values(QueryOperatorsMultiVal).includes(value)) {
|
||||
if (!Array.isArray(query[2].value)) {
|
||||
@ -166,17 +167,8 @@ function QueryConditionField({
|
||||
</QueryConditionContainer>
|
||||
);
|
||||
}
|
||||
const hashCode = (s: string): string => {
|
||||
if (!s) {
|
||||
return '0';
|
||||
}
|
||||
return `${Math.abs(
|
||||
s.split('').reduce((a, b) => {
|
||||
a = (a << 5) - a + b.charCodeAt(0);
|
||||
return a & a;
|
||||
}, 0),
|
||||
)}`;
|
||||
};
|
||||
|
||||
export type Query = { value: string | string[]; type: string }[];
|
||||
|
||||
function QueryBuilder({
|
||||
updateParsedQuery,
|
||||
@ -201,12 +193,9 @@ function QueryBuilder({
|
||||
}
|
||||
}, [parsedQuery]);
|
||||
|
||||
const handleUpdate = (
|
||||
query: { value: string | string[]; type: string }[],
|
||||
queryIndex: number,
|
||||
): void => {
|
||||
const handleUpdate = (query: Query, queryIndex: number): void => {
|
||||
const updatedParsedQuery = generatedQueryStructure;
|
||||
updatedParsedQuery[queryIndex] = query as never;
|
||||
updatedParsedQuery[queryIndex] = parseQuery(query) as never;
|
||||
|
||||
const flatParsedQuery = flatten(updatedParsedQuery).filter((q) => q.value);
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
|
||||
|
@ -0,0 +1,37 @@
|
||||
import _set from 'lodash-es/set';
|
||||
|
||||
import { Query } from './QueryBuilder';
|
||||
|
||||
export const parseQuery = (queries: Query): Query => {
|
||||
if (Array.isArray(queries)) {
|
||||
const isContainsPresent = queries.find((e) => e.value === 'CONTAINS');
|
||||
if (isContainsPresent) {
|
||||
// find the index of VALUE to update
|
||||
const valueIndex = queries.findIndex((e) => e.type === 'QUERY_VALUE');
|
||||
if (valueIndex > -1) {
|
||||
// update the value to wrap with ""
|
||||
_set(
|
||||
queries,
|
||||
[valueIndex, 'value'],
|
||||
`'${queries[valueIndex].value || ''}'`,
|
||||
);
|
||||
}
|
||||
return queries;
|
||||
}
|
||||
}
|
||||
return queries;
|
||||
};
|
||||
|
||||
export const hashCode = (s: string): string => {
|
||||
if (!s) {
|
||||
return '0';
|
||||
}
|
||||
return `${Math.abs(
|
||||
s.split('').reduce((a, b) => {
|
||||
// eslint-disable-next-line no-bitwise, no-param-reassign
|
||||
a = (a << 5) - a + b.charCodeAt(0);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return a & a;
|
||||
}, 0),
|
||||
)}`;
|
||||
};
|
@ -2,9 +2,14 @@
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
|
||||
import { QueryTypes } from 'lib/logql/tokens';
|
||||
import { QueryTypes, QueryOperatorsSingleVal } from 'lib/logql/tokens';
|
||||
|
||||
export const queryKOVPair = () => [
|
||||
export interface QueryFields {
|
||||
type: keyof typeof QueryTypes;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const queryKOVPair = (): QueryFields[] => [
|
||||
{
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: null,
|
||||
|
@ -15,9 +15,6 @@ import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { Container, Heading } from './styles';
|
||||
|
||||
interface LogsTableProps {
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||
}
|
||||
function LogsTable({ getLogs }: LogsTableProps): JSX.Element {
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
@ -51,6 +48,7 @@ function LogsTable({ getLogs }: LogsTableProps): JSX.Element {
|
||||
if (isLoading) {
|
||||
return <Spinner height={20} tip="Getting Logs" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container flex="auto">
|
||||
<Heading>
|
||||
@ -86,4 +84,8 @@ const mapDispatchToProps = (
|
||||
getLogs: bindActionCreators(getLogs, dispatch),
|
||||
});
|
||||
|
||||
interface LogsTableProps {
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(LogsTable));
|
||||
|
@ -51,7 +51,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
history.replace(
|
||||
`${
|
||||
ROUTES.TRACE
|
||||
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1&spanAggregateOrder=ascend`,
|
||||
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1`,
|
||||
);
|
||||
};
|
||||
|
||||
@ -103,7 +103,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
history.replace(
|
||||
`${
|
||||
ROUTES.TRACE
|
||||
}?${urlParams.toString()}?selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1&spanAggregateOrder=ascend`,
|
||||
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1`,
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
|
||||
history.push(
|
||||
`${
|
||||
ROUTES.TRACE
|
||||
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false,"operation":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&spanAggregateCurrentPage=1&spanAggregateOrder=ascend`,
|
||||
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false,"operation":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&spanAggregateCurrentPage=1`,
|
||||
);
|
||||
};
|
||||
|
||||
|
63
frontend/src/container/TopNav/AutoRefresh/config.ts
Normal file
63
frontend/src/container/TopNav/AutoRefresh/config.ts
Normal file
@ -0,0 +1,63 @@
|
||||
export const options: IOptions[] = [
|
||||
{
|
||||
label: 'off',
|
||||
key: 'off',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '5s',
|
||||
key: '5s',
|
||||
value: 5000,
|
||||
},
|
||||
{
|
||||
label: '10s',
|
||||
key: '10s',
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: '30s',
|
||||
key: '30s',
|
||||
value: 30000,
|
||||
},
|
||||
{
|
||||
label: '1m',
|
||||
key: '1m',
|
||||
value: 60000,
|
||||
},
|
||||
{
|
||||
label: '5m',
|
||||
key: '5m',
|
||||
value: 300000,
|
||||
},
|
||||
{
|
||||
label: '10m',
|
||||
key: '10m',
|
||||
value: 600000,
|
||||
},
|
||||
{
|
||||
label: '30m',
|
||||
key: '30m',
|
||||
value: 1800000,
|
||||
},
|
||||
{
|
||||
label: '1h',
|
||||
key: '1h',
|
||||
value: 3600000,
|
||||
},
|
||||
{
|
||||
label: '2h',
|
||||
key: '2h',
|
||||
value: 7200000,
|
||||
},
|
||||
{
|
||||
label: '1d',
|
||||
key: '1d',
|
||||
value: 86400000,
|
||||
},
|
||||
];
|
||||
|
||||
export interface IOptions {
|
||||
label: string;
|
||||
key: string;
|
||||
value: number;
|
||||
}
|
166
frontend/src/container/TopNav/AutoRefresh/index.tsx
Normal file
166
frontend/src/container/TopNav/AutoRefresh/index.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import { CaretDownFilled } from '@ant-design/icons';
|
||||
import {
|
||||
Checkbox,
|
||||
Divider,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
||||
import get from 'api/browser/localstorage/get';
|
||||
import set from 'api/browser/localstorage/set';
|
||||
import { DASHBOARD_TIME_IN_DURATION } from 'constants/app';
|
||||
import dayjs from 'dayjs';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import _omit from 'lodash-es/omit';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useInterval } from 'react-use';
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { options } from './config';
|
||||
import { ButtonContainer, Container } from './styles';
|
||||
|
||||
function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element {
|
||||
const { minTime: initialMinTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const localStorageData = JSON.parse(get(DASHBOARD_TIME_IN_DURATION) || '{}');
|
||||
|
||||
const localStorageValue = useMemo(() => localStorageData[pathname], [
|
||||
pathname,
|
||||
localStorageData,
|
||||
]);
|
||||
|
||||
const [isAutoRefreshEnabled, setIsAutoRefreshfreshEnabled] = useState<boolean>(
|
||||
Boolean(localStorageValue),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAutoRefreshfreshEnabled(Boolean(localStorageValue));
|
||||
}, [localStorageValue]);
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const [selectedOption, setSelectedOption] = useState<string>(
|
||||
localStorageValue || options[0].key,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOption(localStorageValue || options[0].key);
|
||||
}, [localStorageValue, params]);
|
||||
|
||||
const getOption = useMemo(
|
||||
() => options.find((option) => option.key === selectedOption),
|
||||
[selectedOption],
|
||||
);
|
||||
|
||||
useInterval(() => {
|
||||
const selectedValue = getOption?.value;
|
||||
|
||||
if (disabled || !isAutoRefreshEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedOption !== 'off' && selectedValue) {
|
||||
const min = initialMinTime / 1000000;
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
maxTime: dayjs().valueOf() * 1000000,
|
||||
minTime: dayjs(min).subtract(selectedValue, 'second').valueOf() * 1000000,
|
||||
selectedTime,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, getOption?.value || 0);
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(event: RadioChangeEvent) => {
|
||||
const selectedValue = event.target.value;
|
||||
setSelectedOption(selectedValue);
|
||||
params.set(DASHBOARD_TIME_IN_DURATION, selectedValue);
|
||||
set(
|
||||
DASHBOARD_TIME_IN_DURATION,
|
||||
JSON.stringify({ ...localStorageData, [pathname]: selectedValue }),
|
||||
);
|
||||
setIsAutoRefreshfreshEnabled(true);
|
||||
},
|
||||
[params, pathname, localStorageData],
|
||||
);
|
||||
|
||||
const onChangeAutoRefreshHandler = useCallback(
|
||||
(event: CheckboxChangeEvent) => {
|
||||
const { checked } = event.target;
|
||||
if (!checked) {
|
||||
// remove the path from localstorage
|
||||
set(
|
||||
DASHBOARD_TIME_IN_DURATION,
|
||||
JSON.stringify(_omit(localStorageData, pathname)),
|
||||
);
|
||||
}
|
||||
setIsAutoRefreshfreshEnabled(checked);
|
||||
},
|
||||
[localStorageData, pathname],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="bottomLeft"
|
||||
trigger={['click']}
|
||||
content={
|
||||
<Container>
|
||||
<Checkbox
|
||||
onChange={onChangeAutoRefreshHandler}
|
||||
checked={isAutoRefreshEnabled}
|
||||
disabled={disabled}
|
||||
>
|
||||
Auto Refresh
|
||||
</Checkbox>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Typography.Paragraph>Refresh Interval</Typography.Paragraph>
|
||||
|
||||
<Radio.Group onChange={onChangeHandler} value={selectedOption}>
|
||||
<Space direction="vertical">
|
||||
{options
|
||||
.filter((e) => e.label !== 'off')
|
||||
.map((option) => (
|
||||
<Radio key={option.key} value={option.key}>
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Container>
|
||||
}
|
||||
>
|
||||
<ButtonContainer title="Set auto refresh" type="primary">
|
||||
<CaretDownFilled />
|
||||
</ButtonContainer>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
interface AutoRefreshProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
AutoRefresh.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default AutoRefresh;
|
13
frontend/src/container/TopNav/AutoRefresh/styles.ts
Normal file
13
frontend/src/container/TopNav/AutoRefresh/styles.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
min-width: 8rem;
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled(Button)`
|
||||
&&& {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
`;
|
@ -2,7 +2,10 @@ import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { RefreshTextContainer, Typography } from './styles';
|
||||
|
||||
function RefreshText({ onLastRefreshHandler }: RefreshTextProps): JSX.Element {
|
||||
function RefreshText({
|
||||
onLastRefreshHandler,
|
||||
refreshButtonHidden,
|
||||
}: RefreshTextProps): JSX.Element {
|
||||
const [refreshText, setRefreshText] = useState<string>('');
|
||||
|
||||
// this is to update the refresh text
|
||||
@ -19,7 +22,7 @@ function RefreshText({ onLastRefreshHandler }: RefreshTextProps): JSX.Element {
|
||||
}, [onLastRefreshHandler, refreshText]);
|
||||
|
||||
return (
|
||||
<RefreshTextContainer>
|
||||
<RefreshTextContainer refreshButtonHidden={refreshButtonHidden}>
|
||||
<Typography>{refreshText}</Typography>
|
||||
</RefreshTextContainer>
|
||||
);
|
||||
@ -27,6 +30,7 @@ function RefreshText({ onLastRefreshHandler }: RefreshTextProps): JSX.Element {
|
||||
|
||||
interface RefreshTextProps {
|
||||
onLastRefreshHandler: () => string;
|
||||
refreshButtonHidden: boolean;
|
||||
}
|
||||
|
||||
export default RefreshText;
|
||||
|
@ -67,3 +67,19 @@ export const getOptions = (routes: string): Option[] => {
|
||||
}
|
||||
return Options;
|
||||
};
|
||||
|
||||
export const routesToSkip = [
|
||||
ROUTES.SETTINGS,
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
ROUTES.TRACE_DETAIL,
|
||||
ROUTES.ALL_CHANNELS,
|
||||
ROUTES.USAGE_EXPLORER,
|
||||
ROUTES.INSTRUMENTATION,
|
||||
ROUTES.VERSION,
|
||||
ROUTES.ALL_DASHBOARD,
|
||||
ROUTES.ORG_SETTINGS,
|
||||
ROUTES.ERROR_DETAIL,
|
||||
ROUTES.ALERTS_NEW,
|
||||
ROUTES.EDIT_ALERTS,
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
];
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { Button, Select as DefaultSelect } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
@ -14,10 +15,11 @@ import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import AutoRefresh from '../AutoRefresh';
|
||||
import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal';
|
||||
import { getDefaultOption, getOptions, Time } from './config';
|
||||
import RefreshText from './Refresh';
|
||||
import { Container, Form, FormItem } from './styles';
|
||||
import { Form, FormContainer, FormItem } from './styles';
|
||||
|
||||
const { Option } = DefaultSelect;
|
||||
|
||||
@ -240,6 +242,8 @@ function DateTimeSelection({
|
||||
setStartTime(dayjs(preStartTime));
|
||||
setEndTime(dayjs(preEndTime));
|
||||
|
||||
setRefreshButtonHidden(updatedTime === 'custom');
|
||||
|
||||
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
|
||||
}, [
|
||||
location.pathname,
|
||||
@ -253,35 +257,44 @@ function DateTimeSelection({
|
||||
]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<>
|
||||
<Form
|
||||
form={formSelector}
|
||||
layout="inline"
|
||||
initialValues={{ interval: selectedTime }}
|
||||
>
|
||||
<DefaultSelect
|
||||
onSelect={(value: unknown): void => onSelectHandler(value as Time)}
|
||||
value={getInputLabel(startTime, endTime, selectedTime)}
|
||||
data-testid="dropDown"
|
||||
>
|
||||
{options.map(({ value, label }) => (
|
||||
<Option key={value + label} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
</DefaultSelect>
|
||||
<FormContainer>
|
||||
<DefaultSelect
|
||||
onSelect={(value: unknown): void => onSelectHandler(value as Time)}
|
||||
value={getInputLabel(startTime, endTime, selectedTime)}
|
||||
data-testid="dropDown"
|
||||
>
|
||||
{options.map(({ value, label }) => (
|
||||
<Option key={value + label} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
</DefaultSelect>
|
||||
|
||||
<FormItem hidden={refreshButtonHidden}>
|
||||
<Button type="primary" onClick={onRefreshHandler}>
|
||||
Refresh
|
||||
</Button>
|
||||
</FormItem>
|
||||
<FormItem hidden={refreshButtonHidden}>
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
type="primary"
|
||||
onClick={onRefreshHandler}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<AutoRefresh disabled={refreshButtonHidden} />
|
||||
</FormItem>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
|
||||
<RefreshText
|
||||
{...{
|
||||
onLastRefreshHandler,
|
||||
}}
|
||||
refreshButtonHidden={refreshButtonHidden}
|
||||
/>
|
||||
|
||||
<CustomDateTimeModal
|
||||
@ -291,7 +304,7 @@ function DateTimeSelection({
|
||||
setCustomDTPickerVisible(false);
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { Form as FormComponent, Typography as TypographyComponent } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
export const Form = styled(FormComponent)`
|
||||
&&& {
|
||||
justify-content: flex-end;
|
||||
@ -23,6 +19,17 @@ export const FormItem = styled(Form.Item)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const RefreshTextContainer = styled.div`
|
||||
interface Props {
|
||||
refreshButtonHidden: boolean;
|
||||
}
|
||||
|
||||
export const RefreshTextContainer = styled.div<Props>`
|
||||
min-height: 2rem;
|
||||
visibility: ${({ refreshButtonHidden }): string =>
|
||||
refreshButtonHidden ? 'hidden' : 'visible'};
|
||||
`;
|
||||
|
||||
export const FormContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 0.1rem;
|
||||
`;
|
||||
|
@ -1,52 +1,40 @@
|
||||
import { Col } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import React from 'react';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
import React, { useMemo } from 'react';
|
||||
import { matchPath, useHistory } from 'react-router-dom';
|
||||
|
||||
import ShowBreadcrumbs from './Breadcrumbs';
|
||||
import DateTimeSelector from './DateTimeSelection';
|
||||
import { routesToSkip } from './DateTimeSelection/config';
|
||||
import { Container } from './styles';
|
||||
|
||||
const routesToSkip = [
|
||||
ROUTES.SETTINGS,
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
ROUTES.TRACE_DETAIL,
|
||||
ROUTES.ALL_CHANNELS,
|
||||
ROUTES.USAGE_EXPLORER,
|
||||
ROUTES.INSTRUMENTATION,
|
||||
ROUTES.VERSION,
|
||||
ROUTES.ALL_DASHBOARD,
|
||||
ROUTES.ORG_SETTINGS,
|
||||
ROUTES.ERROR_DETAIL,
|
||||
ROUTES.ALERTS_NEW,
|
||||
ROUTES.EDIT_ALERTS,
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
];
|
||||
|
||||
function TopNav(): JSX.Element | null {
|
||||
if (history.location.pathname === ROUTES.SIGN_UP) {
|
||||
const { location } = useHistory();
|
||||
|
||||
const isRouteToSkip = useMemo(
|
||||
() =>
|
||||
routesToSkip.some((route) =>
|
||||
matchPath(location.pathname, { path: route, exact: true }),
|
||||
),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const isSignUpPage = useMemo(
|
||||
() => matchPath(location.pathname, { path: ROUTES.SIGN_UP, exact: true }),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
if (isSignUpPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkRouteExists = (currentPath: string): boolean => {
|
||||
for (let i = 0; i < routesToSkip.length; i += 1) {
|
||||
if (
|
||||
matchPath(currentPath, { path: routesToSkip[i], exact: true, strict: true })
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Col span={16}>
|
||||
<ShowBreadcrumbs />
|
||||
</Col>
|
||||
|
||||
{!checkRouteExists(history.location.pathname) && (
|
||||
{!isRouteToSkip && (
|
||||
<Col span={8}>
|
||||
<DateTimeSelector />
|
||||
</Col>
|
||||
|
@ -3,7 +3,6 @@ import Table, { ColumnsType } from 'antd/lib/table';
|
||||
import ROUTES from 'constants/routes';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import history from 'lib/history';
|
||||
import omit from 'lodash-es/omit';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@ -195,7 +194,7 @@ function TraceTable(): JSX.Element {
|
||||
onClick: (event): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
history.push(getLink(record));
|
||||
window.open(getLink(record));
|
||||
},
|
||||
})}
|
||||
pagination={{
|
||||
|
@ -2,6 +2,7 @@ import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
|
||||
export const UpdateTimeInterval = (
|
||||
interval: Time,
|
||||
@ -11,7 +12,7 @@ export const UpdateTimeInterval = (
|
||||
const { maxTime, minTime } = GetMinMax(interval, dateTimeRange);
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_TIME_INTERVAL',
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
maxTime,
|
||||
minTime,
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
LOGGED_IN,
|
||||
SIDEBAR_COLLAPSE,
|
||||
SWITCH_DARK_MODE,
|
||||
UPDATE_CONFIGS,
|
||||
UPDATE_CURRENT_ERROR,
|
||||
UPDATE_CURRENT_VERSION,
|
||||
UPDATE_FEATURE_FLAGS,
|
||||
@ -56,6 +57,7 @@ const InitialValue: InitialValueTypes = {
|
||||
isUserFetchingError: false,
|
||||
org: null,
|
||||
role: null,
|
||||
configs: {},
|
||||
};
|
||||
|
||||
const appReducer = (
|
||||
@ -210,6 +212,13 @@ const appReducer = (
|
||||
};
|
||||
}
|
||||
|
||||
case UPDATE_CONFIGS: {
|
||||
return {
|
||||
...state,
|
||||
configs: action.payload.configs,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ export const UPDATE_USER = 'UPDATE_USER';
|
||||
export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME';
|
||||
export const UPDATE_ORG = 'UPDATE_ORG';
|
||||
export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS';
|
||||
export const UPDATE_CONFIGS = 'UPDATE_CONFIGS';
|
||||
|
||||
export interface SwitchDarkMode {
|
||||
type: typeof SWITCH_DARK_MODE;
|
||||
@ -115,6 +116,12 @@ export interface UpdateOrg {
|
||||
org: AppReducer['org'];
|
||||
};
|
||||
}
|
||||
export interface UpdateConfigs {
|
||||
type: typeof UPDATE_CONFIGS;
|
||||
payload: {
|
||||
configs: AppReducer['configs'];
|
||||
};
|
||||
}
|
||||
|
||||
export type AppAction =
|
||||
| SwitchDarkMode
|
||||
@ -129,4 +136,5 @@ export type AppAction =
|
||||
| UpdateUser
|
||||
| UpdateOrgName
|
||||
| UpdateOrg
|
||||
| UpdateFeatureFlags;
|
||||
| UpdateFeatureFlags
|
||||
| UpdateConfigs;
|
||||
|
@ -3,7 +3,8 @@ import { Dashboard, DashboardData } from './getAll';
|
||||
export type Props =
|
||||
| {
|
||||
title: Dashboard['data']['title'];
|
||||
uploadedGrafana: boolean;
|
||||
}
|
||||
| DashboardData;
|
||||
| { DashboardData: DashboardData; uploadedGrafana: boolean };
|
||||
|
||||
export type PayloadProps = Dashboard;
|
||||
|
14
frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts
Normal file
14
frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export interface ConfigProps {
|
||||
enabled: boolean;
|
||||
frontendPositionId: string;
|
||||
components: Array<{
|
||||
href: string;
|
||||
darkIcon: string;
|
||||
lightIcon: string;
|
||||
position: 1;
|
||||
text: string;
|
||||
}>;
|
||||
}
|
||||
export interface PayloadProps {
|
||||
[key: string]: ConfigProps;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { PayloadProps as ConfigPayload } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
|
||||
import { PayloadProps as OrgPayload } from 'types/api/user/getOrganization';
|
||||
import { PayloadProps as UserPayload } from 'types/api/user/getUser';
|
||||
@ -26,4 +27,5 @@ export default interface AppReducer {
|
||||
role: ROLES | null;
|
||||
org: OrgPayload | null;
|
||||
featureFlags: null | FeatureFlagPayload;
|
||||
configs: ConfigPayload;
|
||||
}
|
||||
|
3
go.mod
3
go.mod
@ -14,6 +14,7 @@ require (
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-sqlite3 v1.14.8
|
||||
github.com/minio/minio-go/v6 v6.0.57
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/oklog/oklog v0.3.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f
|
||||
@ -145,4 +146,4 @@ require (
|
||||
k8s.io/client-go v8.0.0+incompatible // indirect
|
||||
)
|
||||
|
||||
replace github.com/prometheus/prometheus => github.com/SigNoz/prometheus v1.9.74
|
||||
replace github.com/prometheus/prometheus => github.com/SigNoz/prometheus v1.9.76
|
||||
|
6
go.sum
6
go.sum
@ -57,8 +57,8 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb h1:bneLSKPf9YUSFmafKx32bynV6QrzViL/s+ZDvQxH1E4=
|
||||
github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb/go.mod h1:JznGDNg9x1cujDKa22RaQOimOvvEfy3nxzDGd8XDgmA=
|
||||
github.com/SigNoz/prometheus v1.9.74 h1:/AcKVZ80Cg4FQ/quMDLO4Ejyeb3KDjgUKveiN+OXVL8=
|
||||
github.com/SigNoz/prometheus v1.9.74/go.mod h1:Y4J9tGDmacMC+EcOTp+EIAn2C1sN+9kE+idyVKadiVM=
|
||||
github.com/SigNoz/prometheus v1.9.76 h1:YQOHezj4Yyu6PHV7/bVR297FQgUMQAAJtCVZ+NslwYk=
|
||||
github.com/SigNoz/prometheus v1.9.76/go.mod h1:Y4J9tGDmacMC+EcOTp+EIAn2C1sN+9kE+idyVKadiVM=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@ -345,6 +345,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mkevac/debugcharts v0.0.0-20191222103121-ae1c48aa8615/go.mod h1:Ad7oeElCZqA1Ufj0U9/liOF4BtVepxRcTvr2ey7zTvM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
|
@ -2729,7 +2729,7 @@ func (r *ClickHouseReader) GetMetricAutocompleteTagKey(ctx context.Context, para
|
||||
tagsWhereClause := ""
|
||||
|
||||
for key, val := range params.MetricTags {
|
||||
tagsWhereClause += fmt.Sprintf(" AND labels_object.%s = '%s' ", key, val)
|
||||
tagsWhereClause += fmt.Sprintf(" AND JSONExtractString(labels, '%s') = '%s' ", key, val)
|
||||
}
|
||||
// "select distinctTagKeys from (SELECT DISTINCT arrayJoin(tagKeys) distinctTagKeys from (SELECT DISTINCT(JSONExtractKeys(labels)) tagKeys from signoz_metrics.time_series WHERE JSONExtractString(labels,'__name__')='node_udp_queues')) WHERE distinctTagKeys ILIKE '%host%';"
|
||||
if len(params.Match) != 0 {
|
||||
@ -2768,16 +2768,16 @@ func (r *ClickHouseReader) GetMetricAutocompleteTagValue(ctx context.Context, pa
|
||||
tagsWhereClause := ""
|
||||
|
||||
for key, val := range params.MetricTags {
|
||||
tagsWhereClause += fmt.Sprintf(" AND labels_object.%s = '%s' ", key, val)
|
||||
tagsWhereClause += fmt.Sprintf(" AND JSONExtractString(labels, '%s') = '%s' ", key, val)
|
||||
}
|
||||
|
||||
if len(params.Match) != 0 {
|
||||
query = fmt.Sprintf("SELECT DISTINCT(labels_object.%s) from %s.%s WHERE metric_name=$1 %s AND labels_object.%s ILIKE $2;", params.TagKey, signozMetricDBName, signozTSTableName, tagsWhereClause, params.TagKey)
|
||||
query = fmt.Sprintf("SELECT DISTINCT(JSONExtractString(labels, '%s')) from %s.%s WHERE metric_name=$1 %s AND JSONExtractString(labels, '%s') ILIKE $2;", params.TagKey, signozMetricDBName, signozTSTableName, tagsWhereClause, params.TagKey)
|
||||
|
||||
rows, err = r.db.Query(ctx, query, params.TagKey, params.MetricName, fmt.Sprintf("%%%s%%", params.Match))
|
||||
|
||||
} else {
|
||||
query = fmt.Sprintf("SELECT DISTINCT(labels_object.%s) FROM %s.%s WHERE metric_name=$2 %s;", params.TagKey, signozMetricDBName, signozTSTableName, tagsWhereClause)
|
||||
query = fmt.Sprintf("SELECT DISTINCT(JSONExtractString(labels, '%s')) FROM %s.%s WHERE metric_name=$2 %s;", params.TagKey, signozMetricDBName, signozTSTableName, tagsWhereClause)
|
||||
rows, err = r.db.Query(ctx, query, params.TagKey, params.MetricName)
|
||||
|
||||
}
|
||||
|
@ -4,12 +4,16 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -17,6 +21,14 @@ import (
|
||||
// This time the global variable is unexported.
|
||||
var db *sqlx.DB
|
||||
|
||||
// User for mapping job,instance from grafana
|
||||
var instanceEQRE = regexp.MustCompile("instance(?s)=(?s)\\\"{{.instance}}\\\"")
|
||||
var nodeEQRE = regexp.MustCompile("instance(?s)=(?s)\\\"{{.node}}\\\"")
|
||||
var jobEQRE = regexp.MustCompile("job(?s)=(?s)\\\"{{.job}}\\\"")
|
||||
var instanceRERE = regexp.MustCompile("instance(?s)=~(?s)\\\"{{.instance}}\\\"")
|
||||
var nodeRERE = regexp.MustCompile("instance(?s)=~(?s)\\\"{{.node}}\\\"")
|
||||
var jobRERE = regexp.MustCompile("job(?s)=~(?s)\\\"{{.job}}\\\"")
|
||||
|
||||
// InitDB sets up setting up the connection pool global variable.
|
||||
func InitDB(dataSourceName string) (*sqlx.DB, error) {
|
||||
var err error
|
||||
@ -260,3 +272,236 @@ func SlugifyTitle(title string) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func widgetFromPanel(panel model.Panels, idx int, variables map[string]model.Variable) *model.Widget {
|
||||
widget := model.Widget{
|
||||
Description: panel.Description,
|
||||
ID: strconv.Itoa(idx),
|
||||
IsStacked: false,
|
||||
NullZeroValues: "zero",
|
||||
Opacity: "1",
|
||||
PanelTypes: "TIME_SERIES", // TODO: Need to figure out how to get this
|
||||
Query: model.Query{
|
||||
ClickHouse: []model.ClickHouseQueryDashboard{
|
||||
{
|
||||
Disabled: false,
|
||||
Legend: "",
|
||||
Name: "A",
|
||||
Query: "",
|
||||
},
|
||||
},
|
||||
MetricsBuilder: model.MetricsBuilder{
|
||||
Formulas: []string{},
|
||||
QueryBuilder: []model.QueryBuilder{
|
||||
{
|
||||
AggregateOperator: 1,
|
||||
Disabled: false,
|
||||
GroupBy: []string{},
|
||||
Legend: "",
|
||||
MetricName: "",
|
||||
Name: "A",
|
||||
ReduceTo: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
PromQL: []model.PromQueryDashboard{},
|
||||
QueryType: int(model.PROM),
|
||||
},
|
||||
QueryData: model.QueryDataDashboard{
|
||||
Data: model.Data{
|
||||
QueryData: []interface{}{},
|
||||
},
|
||||
},
|
||||
Title: panel.Title,
|
||||
YAxisUnit: panel.FieldConfig.Defaults.Unit,
|
||||
QueryType: int(model.PROM), // TODO: Supprot for multiple query types
|
||||
}
|
||||
for _, target := range panel.Targets {
|
||||
if target.Expr != "" {
|
||||
for name := range variables {
|
||||
target.Expr = strings.ReplaceAll(target.Expr, "$"+name, "{{"+"."+name+"}}")
|
||||
target.Expr = strings.ReplaceAll(target.Expr, "$"+"__rate_interval", "5m")
|
||||
}
|
||||
|
||||
// prometheus receiver in collector maps job,instance as service_name,service_instance_id
|
||||
target.Expr = instanceEQRE.ReplaceAllString(target.Expr, "service_instance_id=\"{{.instance}}\"")
|
||||
target.Expr = nodeEQRE.ReplaceAllString(target.Expr, "service_instance_id=\"{{.node}}\"")
|
||||
target.Expr = jobEQRE.ReplaceAllString(target.Expr, "service_name=\"{{.job}}\"")
|
||||
target.Expr = instanceRERE.ReplaceAllString(target.Expr, "service_instance_id=~\"{{.instance}}\"")
|
||||
target.Expr = nodeRERE.ReplaceAllString(target.Expr, "service_instance_id=~\"{{.node}}\"")
|
||||
target.Expr = jobRERE.ReplaceAllString(target.Expr, "service_name=~\"{{.job}}\"")
|
||||
|
||||
widget.Query.PromQL = append(
|
||||
widget.Query.PromQL,
|
||||
model.PromQueryDashboard{
|
||||
Disabled: false,
|
||||
Legend: target.LegendFormat,
|
||||
Name: target.RefID,
|
||||
Query: target.Expr,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
return &widget
|
||||
}
|
||||
|
||||
func TransformGrafanaJSONToSignoz(grafanaJSON model.GrafanaJSON) model.DashboardData {
|
||||
var toReturn model.DashboardData
|
||||
toReturn.Title = grafanaJSON.Title
|
||||
toReturn.Tags = grafanaJSON.Tags
|
||||
toReturn.Variables = make(map[string]model.Variable)
|
||||
|
||||
for templateIdx, template := range grafanaJSON.Templating.List {
|
||||
var sort, typ, textboxValue, customValue, queryValue string
|
||||
if template.Sort == 1 {
|
||||
sort = "ASC"
|
||||
} else if template.Sort == 2 {
|
||||
sort = "DESC"
|
||||
} else {
|
||||
sort = "DISABLED"
|
||||
}
|
||||
|
||||
if template.Type == "query" {
|
||||
if template.Datasource == nil {
|
||||
zap.S().Warnf("Skipping panel %d as it has no datasource", templateIdx)
|
||||
continue
|
||||
}
|
||||
// Skip if the source is not prometheus
|
||||
source, stringOk := template.Datasource.(string)
|
||||
if stringOk && !strings.Contains(strings.ToLower(source), "prometheus") {
|
||||
zap.S().Warnf("Skipping template %d as it is not prometheus", templateIdx)
|
||||
continue
|
||||
}
|
||||
var result model.Datasource
|
||||
var structOk bool
|
||||
if reflect.TypeOf(template.Datasource).Kind() == reflect.Map {
|
||||
err := mapstructure.Decode(template.Datasource, &result)
|
||||
if err == nil {
|
||||
structOk = true
|
||||
}
|
||||
}
|
||||
if result.Type != "prometheus" && result.Type != "" {
|
||||
zap.S().Warnf("Skipping template %d as it is not prometheus", templateIdx)
|
||||
continue
|
||||
}
|
||||
|
||||
if !stringOk && !structOk {
|
||||
zap.S().Warnf("Didn't recognize source, skipping")
|
||||
continue
|
||||
}
|
||||
typ = "QUERY"
|
||||
} else if template.Type == "custom" {
|
||||
typ = "CUSTOM"
|
||||
} else if template.Type == "textbox" {
|
||||
typ = "TEXTBOX"
|
||||
text, ok := template.Current.Text.(string)
|
||||
if ok {
|
||||
textboxValue = text
|
||||
}
|
||||
array, ok := template.Current.Text.([]string)
|
||||
if ok {
|
||||
textboxValue = strings.Join(array, ",")
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
var selectedValue string
|
||||
text, ok := template.Current.Value.(string)
|
||||
if ok {
|
||||
selectedValue = text
|
||||
}
|
||||
array, ok := template.Current.Value.([]string)
|
||||
if ok {
|
||||
selectedValue = strings.Join(array, ",")
|
||||
}
|
||||
|
||||
toReturn.Variables[template.Name] = model.Variable{
|
||||
AllSelected: false,
|
||||
CustomValue: customValue,
|
||||
Description: template.Label,
|
||||
MultiSelect: template.Multi,
|
||||
QueryValue: queryValue,
|
||||
SelectedValue: selectedValue,
|
||||
ShowALLOption: template.IncludeAll,
|
||||
Sort: sort,
|
||||
TextboxValue: textboxValue,
|
||||
Type: typ,
|
||||
}
|
||||
}
|
||||
|
||||
row := 0
|
||||
idx := 0
|
||||
for _, panel := range grafanaJSON.Panels {
|
||||
if panel.Type == "row" {
|
||||
if panel.Panels != nil && len(panel.Panels) > 0 {
|
||||
for _, innerPanel := range panel.Panels {
|
||||
if idx%3 == 0 {
|
||||
row++
|
||||
}
|
||||
toReturn.Layout = append(
|
||||
toReturn.Layout,
|
||||
model.Layout{
|
||||
X: idx % 3 * 4,
|
||||
Y: row * 3,
|
||||
W: 4,
|
||||
H: 3,
|
||||
I: strconv.Itoa(idx),
|
||||
},
|
||||
)
|
||||
|
||||
toReturn.Widgets = append(toReturn.Widgets, *widgetFromPanel(innerPanel, idx, toReturn.Variables))
|
||||
idx++
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if panel.Datasource == nil {
|
||||
zap.S().Warnf("Skipping panel %d as it has no datasource", idx)
|
||||
continue
|
||||
}
|
||||
// Skip if the datasource is not prometheus
|
||||
source, stringOk := panel.Datasource.(string)
|
||||
if stringOk && !strings.Contains(strings.ToLower(source), "prometheus") {
|
||||
zap.S().Warnf("Skipping panel %d as it is not prometheus", idx)
|
||||
continue
|
||||
}
|
||||
var result model.Datasource
|
||||
var structOk bool
|
||||
if reflect.TypeOf(panel.Datasource).Kind() == reflect.Map {
|
||||
err := mapstructure.Decode(panel.Datasource, &result)
|
||||
if err == nil {
|
||||
structOk = true
|
||||
}
|
||||
}
|
||||
if result.Type != "prometheus" && result.Type != "" {
|
||||
zap.S().Warnf("Skipping panel %d as it is not prometheus", idx)
|
||||
continue
|
||||
}
|
||||
|
||||
if !stringOk && !structOk {
|
||||
zap.S().Warnf("Didn't recognize source, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a panel from "gridPos"
|
||||
|
||||
if idx%3 == 0 {
|
||||
row++
|
||||
}
|
||||
toReturn.Layout = append(
|
||||
toReturn.Layout,
|
||||
model.Layout{
|
||||
X: idx % 3 * 4,
|
||||
Y: row * 3,
|
||||
W: 4,
|
||||
H: 3,
|
||||
I: strconv.Itoa(idx),
|
||||
},
|
||||
)
|
||||
|
||||
toReturn.Widgets = append(toReturn.Widgets, *widgetFromPanel(panel, idx, toReturn.Variables))
|
||||
idx++
|
||||
}
|
||||
return toReturn
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/dao"
|
||||
am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
|
||||
signozio "go.signoz.io/signoz/pkg/query-service/integrations/signozio"
|
||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/query-service/rules"
|
||||
@ -339,6 +340,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
|
||||
|
||||
router.HandleFunc("/api/v1/dashboards", ViewAccess(aH.getDashboards)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/dashboards", EditAccess(aH.createDashboards)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/dashboards/grafana", EditAccess(aH.createDashboardsTransform)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}", ViewAccess(aH.getDashboard)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.updateDashboard)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete)
|
||||
@ -358,6 +360,8 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
|
||||
router.HandleFunc("/api/v1/settings/ttl", ViewAccess(aH.getTTL)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/version", OpenAccess(aH.getVersion)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/featureFlags", OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/configs", OpenAccess(aH.getConfigs)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/getSpanFilters", ViewAccess(aH.getSpanFilters)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/getTagFilters", ViewAccess(aH.getTagFilters)).Methods(http.MethodPost)
|
||||
@ -822,6 +826,40 @@ func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
func (aH *APIHandler) saveAndReturn(w http.ResponseWriter, signozDashboard model.DashboardData) {
|
||||
toSave := make(map[string]interface{})
|
||||
toSave["title"] = signozDashboard.Title
|
||||
toSave["description"] = signozDashboard.Description
|
||||
toSave["tags"] = signozDashboard.Tags
|
||||
toSave["layout"] = signozDashboard.Layout
|
||||
toSave["widgets"] = signozDashboard.Widgets
|
||||
toSave["variables"] = signozDashboard.Variables
|
||||
|
||||
dashboard, apiError := dashboards.CreateDashboard(toSave)
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, dashboard)
|
||||
return
|
||||
}
|
||||
|
||||
func (aH *APIHandler) createDashboardsTransform(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
defer r.Body.Close()
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
|
||||
var importData model.GrafanaJSON
|
||||
|
||||
err = json.Unmarshal(b, &importData)
|
||||
if err == nil {
|
||||
signozDashboard := dashboards.TransformGrafanaJSONToSignoz(importData)
|
||||
aH.saveAndReturn(w, signozDashboard)
|
||||
return
|
||||
}
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error while creating dashboard from grafana json")
|
||||
}
|
||||
|
||||
func (aH *APIHandler) createDashboards(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var postData map[string]interface{}
|
||||
@ -1422,7 +1460,7 @@ func (aH *APIHandler) getSpanFilters(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (aH *APIHandler) getFilteredSpans(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
query, err := parseFilteredSpansRequest(r)
|
||||
query, err := parseFilteredSpansRequest(r, aH)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
@ -1533,6 +1571,30 @@ func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
|
||||
aH.WriteJSON(w, r, map[string]string{"version": version, "ee": "N"})
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
featureSet := aH.FF().GetFeatureFlags()
|
||||
aH.Respond(w, featureSet)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) FF() interfaces.FeatureLookup {
|
||||
return aH.featureFlags
|
||||
}
|
||||
|
||||
func (aH *APIHandler) CheckFeature(f string) bool {
|
||||
err := aH.FF().CheckFeature(f)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
configs, err := signozio.FetchDynamicConfigs()
|
||||
if err != nil {
|
||||
aH.HandleError(w, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, configs)
|
||||
}
|
||||
|
||||
// inviteUser is used to invite a user. It is used by an admin api.
|
||||
func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := parseInviteRequest(r)
|
||||
|
@ -36,7 +36,7 @@ const (
|
||||
DESC = "desc"
|
||||
)
|
||||
|
||||
var tokenRegex, _ = regexp.Compile(`(?i)(and( )*?|or( )*?)?(([\w.-]+ (in|nin) \([^(]+\))|([\w.]+ (gt|lt|gte|lte) (')?[\S]+(')?)|([\w.]+ (contains|ncontains)) '[^']+')`)
|
||||
var tokenRegex, _ = regexp.Compile(`(?i)(and( )*?|or( )*?)?(([\w.-]+ (in|nin) \([^(]+\))|([\w.]+ (gt|lt|gte|lte) (')?[\S]+(')?)|([\w.]+ (contains|ncontains)) [^\\]?'(.*?[^\\])')`)
|
||||
var operatorRegex, _ = regexp.Compile(`(?i)(?: )(in|nin|gt|lt|gte|lte|contains|ncontains)(?: )`)
|
||||
|
||||
func ParseLogFilterParams(r *http.Request) (*model.LogsFilterParams, error) {
|
||||
|
@ -29,8 +29,8 @@ var correctQueriesTest = []struct {
|
||||
},
|
||||
{
|
||||
`contains search with a different attributes`,
|
||||
`resource contains 'Hello, "World"'`,
|
||||
[]string{`resource ILIKE '%Hello, "World"%' `},
|
||||
`resource contains 'Hello, "World" and user\'s'`,
|
||||
[]string{`resource ILIKE '%Hello, "World" and user\'s%' `},
|
||||
},
|
||||
{
|
||||
`more than one continas`,
|
||||
|
@ -111,21 +111,21 @@ func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string,
|
||||
fmtVal := FormattedValue(toFormat)
|
||||
switch op {
|
||||
case "eq":
|
||||
conditions = append(conditions, fmt.Sprintf("labels_object.%s = %s", item.Key, fmtVal))
|
||||
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') = %s", item.Key, fmtVal))
|
||||
case "neq":
|
||||
conditions = append(conditions, fmt.Sprintf("labels_object.%s != %s", item.Key, fmtVal))
|
||||
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') != %s", item.Key, fmtVal))
|
||||
case "in":
|
||||
conditions = append(conditions, fmt.Sprintf("labels_object.%s IN %s", item.Key, fmtVal))
|
||||
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') IN %s", item.Key, fmtVal))
|
||||
case "nin":
|
||||
conditions = append(conditions, fmt.Sprintf("labels_object.%s NOT IN %s", item.Key, fmtVal))
|
||||
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') NOT IN %s", item.Key, fmtVal))
|
||||
case "like":
|
||||
conditions = append(conditions, fmt.Sprintf("like(labels_object.%s, %s)", item.Key, fmtVal))
|
||||
conditions = append(conditions, fmt.Sprintf("like(JSONExtractString(labels, '%s'), %s)", item.Key, fmtVal))
|
||||
case "nlike":
|
||||
conditions = append(conditions, fmt.Sprintf("notLike(labels_object.%s, %s)", item.Key, fmtVal))
|
||||
conditions = append(conditions, fmt.Sprintf("notLike(JSONExtractString(labels, '%s'), %s)", item.Key, fmtVal))
|
||||
case "match":
|
||||
conditions = append(conditions, fmt.Sprintf("match(labels_object.%s, %s)", item.Key, fmtVal))
|
||||
conditions = append(conditions, fmt.Sprintf("match(JSONExtractString(labels, '%s'), %s)", item.Key, fmtVal))
|
||||
case "nmatch":
|
||||
conditions = append(conditions, fmt.Sprintf("not match(labels_object.%s, %s)", item.Key, fmtVal))
|
||||
conditions = append(conditions, fmt.Sprintf("not match(JSONExtractString(labels, '%s'), %s)", item.Key, fmtVal))
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported operation")
|
||||
}
|
||||
@ -138,7 +138,7 @@ func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string,
|
||||
selectLabels = "labels,"
|
||||
} else {
|
||||
for _, tag := range groupTags {
|
||||
selectLabels += fmt.Sprintf(" labels_object.%s as %s,", tag, tag)
|
||||
selectLabels += fmt.Sprintf(" JSONExtractString(labels, '%s') as %s,", tag, tag)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,9 +55,9 @@ func TestBuildQueryWithFilters(t *testing.T) {
|
||||
queries := PrepareBuilderMetricQueries(q, "table").Queries
|
||||
So(len(queries), ShouldEqual, 1)
|
||||
|
||||
So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND labels_object.a != 'b'")
|
||||
So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'a') != 'b'")
|
||||
So(queries["a"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
|
||||
So(queries["a"], ShouldContainSubstring, "not match(labels_object.code, 'ERROR_*')")
|
||||
So(queries["a"], ShouldContainSubstring, "not match(JSONExtractString(labels, 'code'), 'ERROR_*')")
|
||||
})
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ func TestBuildQueryWithMultipleQueries(t *testing.T) {
|
||||
}
|
||||
queries := PrepareBuilderMetricQueries(q, "table").Queries
|
||||
So(len(queries), ShouldEqual, 2)
|
||||
So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND labels_object.in IN ['a','b','c']")
|
||||
So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']")
|
||||
So(queries["a"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
|
||||
})
|
||||
}
|
||||
@ -126,7 +126,7 @@ func TestBuildQueryWithMultipleQueriesAndFormula(t *testing.T) {
|
||||
queries := PrepareBuilderMetricQueries(q, "table").Queries
|
||||
So(len(queries), ShouldEqual, 3)
|
||||
So(queries["c"], ShouldContainSubstring, "SELECT ts, a.value / b.value")
|
||||
So(queries["c"], ShouldContainSubstring, "WHERE metric_name = 'name' AND labels_object.in IN ['a','b','c']")
|
||||
So(queries["c"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']")
|
||||
So(queries["c"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
|
||||
})
|
||||
}
|
||||
|
@ -255,7 +255,7 @@ func parseSpanFilterRequestBody(r *http.Request) (*model.SpanFilterParams, error
|
||||
return postData, nil
|
||||
}
|
||||
|
||||
func parseFilteredSpansRequest(r *http.Request) (*model.GetFilteredSpansParams, error) {
|
||||
func parseFilteredSpansRequest(r *http.Request, aH *APIHandler) (*model.GetFilteredSpansParams, error) {
|
||||
|
||||
var postData *model.GetFilteredSpansParams
|
||||
err := json.NewDecoder(r.Body).Decode(&postData)
|
||||
@ -277,6 +277,20 @@ func parseFilteredSpansRequest(r *http.Request) (*model.GetFilteredSpansParams,
|
||||
postData.Limit = 10
|
||||
}
|
||||
|
||||
if len(postData.Order) != 0 {
|
||||
if postData.Order != constants.Ascending && postData.Order != constants.Descending {
|
||||
return nil, errors.New("order param is not in correct format")
|
||||
}
|
||||
if postData.OrderParam != constants.Duration && postData.OrderParam != constants.Timestamp {
|
||||
return nil, errors.New("order param is not in correct format")
|
||||
}
|
||||
if postData.OrderParam == constants.Duration && !aH.CheckFeature(constants.DurationSort) {
|
||||
return nil, model.ErrFeatureUnavailable{Key: constants.DurationSort}
|
||||
} else if postData.OrderParam == constants.Timestamp && !aH.CheckFeature(constants.TimestampSort) {
|
||||
return nil, model.ErrFeatureUnavailable{Key: constants.TimestampSort}
|
||||
}
|
||||
}
|
||||
|
||||
return postData, nil
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ func TestParseFilterSingleFilter(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "", bytes.NewReader(postBody))
|
||||
res, _ := parseFilterSet(req)
|
||||
query, _ := metrics.BuildMetricsTimeSeriesFilterQuery(res, []string{}, "table", model.NOOP)
|
||||
So(query, ShouldContainSubstring, "signoz_metrics.time_series_v2 WHERE metric_name = 'table' AND labels_object.namespace = 'a'")
|
||||
So(query, ShouldContainSubstring, "signoz_metrics.time_series_v2 WHERE metric_name = 'table' AND JSONExtractString(labels, 'namespace') = 'a'")
|
||||
})
|
||||
}
|
||||
|
||||
@ -39,8 +39,8 @@ func TestParseFilterMultipleFilter(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "", bytes.NewReader(postBody))
|
||||
res, _ := parseFilterSet(req)
|
||||
query, _ := metrics.BuildMetricsTimeSeriesFilterQuery(res, []string{}, "table", model.NOOP)
|
||||
So(query, should.ContainSubstring, "labels_object.host IN ['host-1','host-2']")
|
||||
So(query, should.ContainSubstring, "labels_object.namespace = 'a'")
|
||||
So(query, should.ContainSubstring, "JSONExtractString(labels, 'host') IN ['host-1','host-2']")
|
||||
So(query, should.ContainSubstring, "JSONExtractString(labels, 'namespace') = 'a'")
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/query-service/dao"
|
||||
"go.signoz.io/signoz/pkg/query-service/featureManager"
|
||||
"go.signoz.io/signoz/pkg/query-service/healthcheck"
|
||||
am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
|
||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
@ -77,6 +78,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
localDB.SetMaxOpenConns(10)
|
||||
|
||||
// initiate feature manager
|
||||
fm := featureManager.StartManager()
|
||||
|
||||
readerReady := make(chan bool)
|
||||
|
||||
var reader interfaces.Reader
|
||||
@ -98,9 +103,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
|
||||
telemetry.GetInstance().SetReader(reader)
|
||||
apiHandler, err := NewAPIHandler(APIHandlerOpts{
|
||||
Reader: reader,
|
||||
AppDao: dao.DB(),
|
||||
RuleManager: rm,
|
||||
Reader: reader,
|
||||
AppDao: dao.DB(),
|
||||
RuleManager: rm,
|
||||
FeatureFlags: fm,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -13,6 +13,8 @@ const (
|
||||
DebugHttpPort = "0.0.0.0:6060" // Address to serve http (pprof)
|
||||
)
|
||||
|
||||
var ConfigSignozIo = "https://config.signoz.io/api/v1"
|
||||
|
||||
var DEFAULT_TELEMETRY_ANONYMOUS = false
|
||||
|
||||
func IsTelemetryEnabled() bool {
|
||||
@ -28,6 +30,9 @@ const TraceTTL = "traces"
|
||||
const MetricsTTL = "metrics"
|
||||
const LogsTTL = "logs"
|
||||
|
||||
const DurationSort = "DurationSort"
|
||||
const TimestampSort = "TimestampSort"
|
||||
|
||||
func GetAlertManagerApiPrefix() string {
|
||||
if os.Getenv("ALERTMANAGER_API_PREFIX") != "" {
|
||||
return os.Getenv("ALERTMANAGER_API_PREFIX")
|
||||
@ -40,6 +45,33 @@ var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/rout
|
||||
|
||||
var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db")
|
||||
|
||||
var DurationSortFeature = GetOrDefaultEnv("DURATION_SORT_FEATURE", "true")
|
||||
|
||||
var TimestampSortFeature = GetOrDefaultEnv("TIMESTAMP_SORT_FEATURE", "true")
|
||||
|
||||
func IsDurationSortFeatureEnabled() bool {
|
||||
isDurationSortFeatureEnabledStr := DurationSortFeature
|
||||
isDurationSortFeatureEnabledBool, err := strconv.ParseBool(isDurationSortFeatureEnabledStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return isDurationSortFeatureEnabledBool
|
||||
}
|
||||
|
||||
func IsTimestampSortFeatureEnabled() bool {
|
||||
isTimestampSortFeatureEnabledStr := TimestampSortFeature
|
||||
isTimestampSortFeatureEnabledBool, err := strconv.ParseBool(isTimestampSortFeatureEnabledStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return isTimestampSortFeatureEnabledBool
|
||||
}
|
||||
|
||||
var DEFAULT_FEATURE_SET = model.FeatureSet{
|
||||
DurationSort: IsDurationSortFeatureEnabled(),
|
||||
TimestampSort: IsTimestampSortFeatureEnabled(),
|
||||
}
|
||||
|
||||
const (
|
||||
TraceID = "traceID"
|
||||
ServiceName = "serviceName"
|
||||
|
34
pkg/query-service/featureManager/manager.go
Normal file
34
pkg/query-service/featureManager/manager.go
Normal file
@ -0,0 +1,34 @@
|
||||
package featureManager
|
||||
|
||||
import (
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
)
|
||||
|
||||
type FeatureManager struct {
|
||||
activeFeatures model.FeatureSet
|
||||
}
|
||||
|
||||
func StartManager() *FeatureManager {
|
||||
fM := &FeatureManager{
|
||||
activeFeatures: constants.DEFAULT_FEATURE_SET,
|
||||
}
|
||||
return fM
|
||||
}
|
||||
|
||||
// CheckFeature will be internally used by backend routines
|
||||
// for feature gating
|
||||
func (fm *FeatureManager) CheckFeature(featureKey string) error {
|
||||
if value, ok := fm.activeFeatures[featureKey]; ok {
|
||||
if value {
|
||||
return nil
|
||||
}
|
||||
return model.ErrFeatureUnavailable{Key: featureKey}
|
||||
}
|
||||
return model.ErrFeatureUnavailable{Key: featureKey}
|
||||
}
|
||||
|
||||
// GetFeatureFlags returns current active features
|
||||
func (fm *FeatureManager) GetFeatureFlags() model.FeatureSet {
|
||||
return fm.activeFeatures
|
||||
}
|
75
pkg/query-service/integrations/signozio/dynamic_config.go
Normal file
75
pkg/query-service/integrations/signozio/dynamic_config.go
Normal file
@ -0,0 +1,75 @@
|
||||
package signozio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
)
|
||||
|
||||
var C *Client
|
||||
|
||||
const (
|
||||
POST = "POST"
|
||||
APPLICATION_JSON = "application/json"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func New() *Client {
|
||||
return &Client{
|
||||
Prefix: constants.ConfigSignozIo,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
C = New()
|
||||
}
|
||||
|
||||
// FetchDynamicConfigs fetches configs from config server
|
||||
func FetchDynamicConfigs() (map[string]Config, *model.ApiError) {
|
||||
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
req, err := http.NewRequest(http.MethodGet, C.Prefix+"/configs", http.NoBody)
|
||||
if err != nil {
|
||||
return DefaultConfig, nil
|
||||
}
|
||||
req.SetBasicAuth("admin", "SigNoz@adm1n")
|
||||
httpResponse, err := client.Do(req)
|
||||
if err != nil {
|
||||
return DefaultConfig, nil
|
||||
}
|
||||
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
return DefaultConfig, nil
|
||||
}
|
||||
|
||||
httpBody, err := ioutil.ReadAll(httpResponse.Body)
|
||||
if err != nil {
|
||||
return DefaultConfig, nil
|
||||
}
|
||||
|
||||
// read api request result
|
||||
result := ConfigResult{}
|
||||
err = json.Unmarshal(httpBody, &result)
|
||||
if err != nil {
|
||||
return DefaultConfig, nil
|
||||
}
|
||||
|
||||
switch httpResponse.StatusCode {
|
||||
case 200, 201:
|
||||
return result.Data, nil
|
||||
case 400, 401:
|
||||
return DefaultConfig, nil
|
||||
default:
|
||||
return DefaultConfig, nil
|
||||
}
|
||||
|
||||
}
|
54
pkg/query-service/integrations/signozio/response.go
Normal file
54
pkg/query-service/integrations/signozio/response.go
Normal file
@ -0,0 +1,54 @@
|
||||
package signozio
|
||||
|
||||
type status string
|
||||
|
||||
type ConfigResult struct {
|
||||
Status status `json:"status"`
|
||||
Data map[string]Config `json:"data,omitempty"`
|
||||
ErrorType string `json:"errorType,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
FrontendPositionId string `json:"frontendPositionId"`
|
||||
Components []ComponentProps `json:"components"`
|
||||
}
|
||||
|
||||
type ComponentProps struct {
|
||||
Text string `json:"text"`
|
||||
Position int `json:"position"`
|
||||
DarkIcon string `json:"darkIcon"`
|
||||
LightIcon string `json:"lightIcon"`
|
||||
Href string `json:"href"`
|
||||
}
|
||||
|
||||
var DefaultConfig = map[string]Config{
|
||||
"helpConfig": {
|
||||
Enabled: true,
|
||||
FrontendPositionId: "tooltip",
|
||||
Components: []ComponentProps{
|
||||
{
|
||||
Text: "How to use SigNoz in production",
|
||||
Position: 1,
|
||||
LightIcon: "RiseOutlined",
|
||||
DarkIcon: "RiseOutlined",
|
||||
Href: "https://signoz.io/docs/production-readiness",
|
||||
},
|
||||
{
|
||||
Text: "Create an issue in GitHub",
|
||||
Position: 2,
|
||||
LightIcon: "GithubFilled",
|
||||
DarkIcon: "GithubOutlined",
|
||||
Href: "https://github.com/SigNoz/signoz/issues/new/choose",
|
||||
},
|
||||
{
|
||||
Text: "Read the docs",
|
||||
Position: 3,
|
||||
LightIcon: "FileTextFilled",
|
||||
DarkIcon: "FileTextOutlined",
|
||||
Href: "https://signoz.io/docs",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
254
pkg/query-service/model/dashboards.go
Normal file
254
pkg/query-service/model/dashboards.go
Normal file
@ -0,0 +1,254 @@
|
||||
package model
|
||||
|
||||
type Datasource struct {
|
||||
Type string `json:"type"`
|
||||
UID string `json:"uid"`
|
||||
}
|
||||
|
||||
type Panels struct {
|
||||
Datasource interface{} `json:"datasource"`
|
||||
Description string `json:"description,omitempty"`
|
||||
FieldConfig struct {
|
||||
Defaults struct {
|
||||
Color struct {
|
||||
Mode string `json:"mode"`
|
||||
} `json:"color"`
|
||||
Max float64 `json:"max"`
|
||||
Min float64 `json:"min"`
|
||||
Thresholds struct {
|
||||
Mode string `json:"mode"`
|
||||
Steps []struct {
|
||||
Color string `json:"color"`
|
||||
Value interface{} `json:"value"`
|
||||
} `json:"steps"`
|
||||
} `json:"thresholds"`
|
||||
Unit string `json:"unit"`
|
||||
} `json:"defaults"`
|
||||
Overrides []interface{} `json:"overrides"`
|
||||
} `json:"fieldConfig,omitempty"`
|
||||
GridPos struct {
|
||||
H int `json:"h"`
|
||||
W int `json:"w"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
} `json:"gridPos"`
|
||||
ID int `json:"id"`
|
||||
Links []interface{} `json:"links,omitempty"`
|
||||
Options struct {
|
||||
Orientation string `json:"orientation"`
|
||||
ReduceOptions struct {
|
||||
Calcs []string `json:"calcs"`
|
||||
Fields string `json:"fields"`
|
||||
Values bool `json:"values"`
|
||||
} `json:"reduceOptions"`
|
||||
ShowThresholdLabels bool `json:"showThresholdLabels"`
|
||||
ShowThresholdMarkers bool `json:"showThresholdMarkers"`
|
||||
} `json:"options,omitempty"`
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
Targets []struct {
|
||||
Datasource interface{} `json:"datasource"`
|
||||
EditorMode string `json:"editorMode"`
|
||||
Expr string `json:"expr"`
|
||||
Hide bool `json:"hide"`
|
||||
IntervalFactor int `json:"intervalFactor"`
|
||||
LegendFormat string `json:"legendFormat"`
|
||||
Range bool `json:"range"`
|
||||
RefID string `json:"refId"`
|
||||
Step int `json:"step"`
|
||||
} `json:"targets"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
HideTimeOverride bool `json:"hideTimeOverride,omitempty"`
|
||||
MaxDataPoints int `json:"maxDataPoints,omitempty"`
|
||||
Collapsed bool `json:"collapsed,omitempty"`
|
||||
Panels []Panels `json:"panels,omitempty"`
|
||||
}
|
||||
|
||||
type GrafanaJSON struct {
|
||||
Inputs []struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
PluginID string `json:"pluginId"`
|
||||
PluginName string `json:"pluginName"`
|
||||
} `json:"__inputs"`
|
||||
Requires []struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
} `json:"__requires"`
|
||||
Annotations struct {
|
||||
List []struct {
|
||||
HashKey string `json:"$$hashKey"`
|
||||
BuiltIn int `json:"builtIn"`
|
||||
Datasource interface{} `json:"datasource"`
|
||||
Enable bool `json:"enable"`
|
||||
Hide bool `json:"hide"`
|
||||
IconColor string `json:"iconColor"`
|
||||
Name string `json:"name"`
|
||||
Target struct {
|
||||
Limit int `json:"limit"`
|
||||
MatchAny bool `json:"matchAny"`
|
||||
Tags []interface{} `json:"tags"`
|
||||
Type string `json:"type"`
|
||||
} `json:"target"`
|
||||
Type string `json:"type"`
|
||||
} `json:"list"`
|
||||
} `json:"annotations"`
|
||||
Editable bool `json:"editable"`
|
||||
FiscalYearStartMonth int `json:"fiscalYearStartMonth"`
|
||||
GnetID int `json:"gnetId"`
|
||||
GraphTooltip int `json:"graphTooltip"`
|
||||
ID interface{} `json:"id"`
|
||||
Links []struct {
|
||||
Icon string `json:"icon"`
|
||||
Tags []interface{} `json:"tags"`
|
||||
TargetBlank bool `json:"targetBlank"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
} `json:"links"`
|
||||
LiveNow bool `json:"liveNow"`
|
||||
Panels []Panels `json:"panels"`
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
Style string `json:"style"`
|
||||
Tags []string `json:"tags"`
|
||||
Templating struct {
|
||||
List []struct {
|
||||
Current struct {
|
||||
Selected bool `json:"selected"`
|
||||
Text interface{} `json:"text"`
|
||||
Value interface{} `json:"value"`
|
||||
} `json:"current"`
|
||||
Hide int `json:"hide"`
|
||||
IncludeAll bool `json:"includeAll"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Multi bool `json:"multi"`
|
||||
Name string `json:"name"`
|
||||
Options []interface{} `json:"options"`
|
||||
Query interface{} `json:"query"`
|
||||
Refresh int `json:"refresh,omitempty"`
|
||||
Regex string `json:"regex,omitempty"`
|
||||
SkipURLSync bool `json:"skipUrlSync"`
|
||||
Type string `json:"type"`
|
||||
Datasource interface{} `json:"datasource,omitempty"`
|
||||
Definition string `json:"definition,omitempty"`
|
||||
Sort int `json:"sort,omitempty"`
|
||||
TagValuesQuery string `json:"tagValuesQuery,omitempty"`
|
||||
TagsQuery string `json:"tagsQuery,omitempty"`
|
||||
UseTags bool `json:"useTags,omitempty"`
|
||||
} `json:"list"`
|
||||
} `json:"templating"`
|
||||
Time struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
} `json:"time"`
|
||||
Timepicker struct {
|
||||
RefreshIntervals []string `json:"refresh_intervals"`
|
||||
TimeOptions []string `json:"time_options"`
|
||||
} `json:"timepicker"`
|
||||
Timezone string `json:"timezone"`
|
||||
Title string `json:"title"`
|
||||
UID string `json:"uid"`
|
||||
Version int `json:"version"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
}
|
||||
type Layout struct {
|
||||
H int `json:"h"`
|
||||
I string `json:"i"`
|
||||
Moved bool `json:"moved"`
|
||||
Static bool `json:"static"`
|
||||
W int `json:"w"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
type Variable struct {
|
||||
AllSelected bool `json:"allSelected"`
|
||||
CustomValue string `json:"customValue"`
|
||||
Description string `json:"description"`
|
||||
ModificationUUID string `json:"modificationUUID"`
|
||||
MultiSelect bool `json:"multiSelect"`
|
||||
QueryValue string `json:"queryValue"`
|
||||
SelectedValue string `json:"selectedValue"`
|
||||
ShowALLOption bool `json:"showALLOption"`
|
||||
Sort string `json:"sort"`
|
||||
TextboxValue string `json:"textboxValue"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
Legend string `json:"legend"`
|
||||
Query string `json:"query"`
|
||||
QueryData []interface{} `json:"queryData"`
|
||||
}
|
||||
|
||||
type QueryDataDashboard struct {
|
||||
Data Data `json:"data"`
|
||||
Error bool `json:"error"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
Loading bool `json:"loading"`
|
||||
}
|
||||
|
||||
type ClickHouseQueryDashboard struct {
|
||||
Legend string `json:"legend"`
|
||||
Name string `json:"name"`
|
||||
Query string `json:"rawQuery"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type QueryBuilder struct {
|
||||
AggregateOperator interface{} `json:"aggregateOperator"`
|
||||
Disabled bool `json:"disabled"`
|
||||
GroupBy []string `json:"groupBy"`
|
||||
Legend string `json:"legend"`
|
||||
MetricName string `json:"metricName"`
|
||||
Name string `json:"name"`
|
||||
TagFilters TagFilters `json:"tagFilters"`
|
||||
ReduceTo interface{} `json:"reduceTo"`
|
||||
}
|
||||
|
||||
type MetricsBuilder struct {
|
||||
Formulas []string `json:"formulas"`
|
||||
QueryBuilder []QueryBuilder `json:"queryBuilder"`
|
||||
}
|
||||
|
||||
type PromQueryDashboard struct {
|
||||
Query string `json:"query"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Name string `json:"name"`
|
||||
Legend string `json:"legend"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
ClickHouse []ClickHouseQueryDashboard `json:"clickHouse"`
|
||||
PromQL []PromQueryDashboard `json:"promQL"`
|
||||
MetricsBuilder MetricsBuilder `json:"metricsBuilder"`
|
||||
QueryType int `json:"queryType"`
|
||||
}
|
||||
|
||||
type Widget struct {
|
||||
Description string `json:"description"`
|
||||
ID string `json:"id"`
|
||||
IsStacked bool `json:"isStacked"`
|
||||
NullZeroValues string `json:"nullZeroValues"`
|
||||
Opacity string `json:"opacity"`
|
||||
PanelTypes string `json:"panelTypes"`
|
||||
Query Query `json:"query"`
|
||||
QueryData QueryDataDashboard `json:"queryData"`
|
||||
TimePreferance string `json:"timePreferance"`
|
||||
Title string `json:"title"`
|
||||
YAxisUnit string `json:"yAxisUnit"`
|
||||
QueryType int `json:"queryType"`
|
||||
}
|
||||
|
||||
type DashboardData struct {
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
Layout []Layout `json:"layout"`
|
||||
Title string `json:"title"`
|
||||
Widgets []Widget `json:"widgets"`
|
||||
Variables map[string]Variable `json:"variables"`
|
||||
}
|
@ -44,7 +44,7 @@ const HEART_BEAT_DURATION = 6 * time.Hour
|
||||
// const HEART_BEAT_DURATION = 10 * time.Second
|
||||
|
||||
const RATE_LIMIT_CHECK_DURATION = 1 * time.Minute
|
||||
const RATE_LIMIT_VALUE = 60
|
||||
const RATE_LIMIT_VALUE = 10
|
||||
|
||||
// const RATE_LIMIT_CHECK_DURATION = 20 * time.Second
|
||||
// const RATE_LIMIT_VALUE = 5
|
||||
|
@ -2,7 +2,7 @@ version: "2.4"
|
||||
|
||||
services:
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:22.4.5-alpine
|
||||
image: clickhouse/clickhouse-server:22.8.8-alpine
|
||||
tty: true
|
||||
volumes:
|
||||
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
||||
@ -62,7 +62,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.55.3
|
||||
image: signoz/signoz-otel-collector:0.63.0
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -78,7 +78,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.55.3
|
||||
image: signoz/signoz-otel-collector:0.63.0
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -47,7 +47,7 @@ receivers:
|
||||
# thrift_binary:
|
||||
# endpoint: 0.0.0.0:6832
|
||||
hostmetrics:
|
||||
collection_interval: 60s
|
||||
collection_interval: 30s
|
||||
scrapers:
|
||||
cpu: {}
|
||||
load: {}
|
||||
@ -55,6 +55,16 @@ receivers:
|
||||
disk: {}
|
||||
filesystem: {}
|
||||
network: {}
|
||||
prometheus:
|
||||
config:
|
||||
global:
|
||||
scrape_interval: 60s
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: otel-collector
|
||||
static_configs:
|
||||
- targets:
|
||||
- otel-collector:8888
|
||||
|
||||
processors:
|
||||
batch:
|
||||
@ -88,7 +98,6 @@ processors:
|
||||
resourcedetection:
|
||||
detectors: [env, system]
|
||||
timeout: 2s
|
||||
override: false
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
@ -126,8 +135,8 @@ service:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/hostmetrics:
|
||||
receivers: [hostmetrics]
|
||||
metrics/generic:
|
||||
receivers: [hostmetrics, prometheus]
|
||||
processors: [resourcedetection, batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/spanmetrics:
|
||||
|
@ -6,20 +6,14 @@ receivers:
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: "otel-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- otel-collector:8888
|
||||
# otel-collector-metrics internal metrics
|
||||
- job_name: "otel-collector-metrics"
|
||||
- job_name: otel-collector-metrics
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
# SigNoz span metrics
|
||||
- job_name: "signozspanmetrics-collector"
|
||||
- job_name: signozspanmetrics-collector
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
|
Loading…
x
Reference in New Issue
Block a user