Merge pull request #1722 from SigNoz/release/v0.11.3

Release/v0.11.3
This commit is contained in:
Ankit Nayan 2022-11-16 19:50:55 +05:30 committed by GitHub
commit 78d2377520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1673 additions and 309 deletions

View File

@ -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

View File

@ -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 \

View File

@ -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/

View File

@ -13,14 +13,19 @@
##
SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式踪来增加软件技术栈的可见性。
SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式踪来增加软件技术栈的可见性。
👉 你能看到一些性能矩阵服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。
👉 你能看到一些性能指标服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。
👉 通过准确的踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。
👉 通过准确的踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。
👉 聚合trace数据来获得业务相关指标。
![SigNoz Feature](https://signoz-public.s3.us-east-2.amazonaws.com/signoz_hero_github.png)
![screenzy-1644432902955](https://user-images.githubusercontent.com/504541/153270713-1b2156e6-ec03-42de-975b-3c02b8ec1836.png)
<br />
![screenzy-1644432986784](https://user-images.githubusercontent.com/504541/153270725-0efb73b3-06ed-4207-bf13-9b7e2e17c4b8.png)
<br />
![screenzy-1647005040573](https://user-images.githubusercontent.com/504541/157875938-a3d57904-ea6d-4278-b929-bd1408d7f94c.png)
<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>&nbsp </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>&nbsp </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" />

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

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

View File

@ -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",

View File

@ -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",

View File

@ -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,
});

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

View File

@ -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';

View File

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

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

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

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

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

View File

@ -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>

View File

@ -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;
`;

View File

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

View File

@ -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={{

View File

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

View File

@ -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));

View File

@ -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),
)}`;
};

View File

@ -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,

View File

@ -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));

View File

@ -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`,
);
};

View File

@ -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`,
);
};

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

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

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

View File

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

View File

@ -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,
];

View File

@ -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>
</>
);
}

View File

@ -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;
`;

View File

@ -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>

View File

@ -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={{

View File

@ -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,

View File

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

View File

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

View File

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

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

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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)
}

View File

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

View File

@ -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)

View File

@ -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) {

View File

@ -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`,

View File

@ -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)
}
}

View File

@ -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)")
})
}

View File

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

View File

@ -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'")
})
}

View File

@ -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

View File

@ -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"

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

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

View 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",
},
},
},
}

View 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"`
}

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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: