Compare commits
105 Commits
v0.53.0-cl
...
chore/samp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c49ba444b | ||
|
|
262beef8f9 | ||
|
|
43cc6dea92 | ||
|
|
6684640abe | ||
|
|
363fb7bc34 | ||
|
|
dde4485839 | ||
|
|
44598e304d | ||
|
|
4295a2756a | ||
|
|
0a146910d6 | ||
|
|
690ed0f7f1 | ||
|
|
2f0d98ae51 | ||
|
|
fb92ddc822 | ||
|
|
15b0569b56 | ||
|
|
140533b790 | ||
|
|
710d22786d | ||
|
|
532f274bd6 | ||
|
|
3200fd054e | ||
|
|
8468cc863e | ||
|
|
71911687bf | ||
|
|
9644297d28 | ||
|
|
faa6fdfcde | ||
|
|
aabf364cc6 | ||
|
|
4b861b2169 | ||
|
|
8d655bf419 | ||
|
|
90cb8ba9a1 | ||
|
|
f508ee7521 | ||
|
|
413caad0d8 | ||
|
|
666f601ecd | ||
|
|
5cdcbef00c | ||
|
|
c2f607ab6b | ||
|
|
2ca10bb87c | ||
|
|
6fb2a6d4c9 | ||
|
|
464589e0ca | ||
|
|
3b94dab3ce | ||
|
|
9f481aacff | ||
|
|
9eb6c6201b | ||
|
|
22f2e68db2 | ||
|
|
5bcf7de440 | ||
|
|
703983a5f9 | ||
|
|
766a2123c5 | ||
|
|
c13b347808 | ||
|
|
a476c68f7e | ||
|
|
fc15aa6f1c | ||
|
|
4192fd573d | ||
|
|
ca13d80205 | ||
|
|
610edbb3d1 | ||
|
|
8d84ce8f06 | ||
|
|
09ea7b9eb5 | ||
|
|
04991ca4a2 | ||
|
|
9c7e1a0581 | ||
|
|
e3484dcfa9 | ||
|
|
e6de113ff0 | ||
|
|
c8045243b5 | ||
|
|
4fd862e7ff | ||
|
|
950eb99de0 | ||
|
|
078aaafc57 | ||
|
|
04564f63a8 | ||
|
|
862b8b92bd | ||
|
|
7b0f5976b4 | ||
|
|
b5d01705df | ||
|
|
8f1fecc5ba | ||
|
|
902eead23c | ||
|
|
ffa414fc00 | ||
|
|
aa3f1bae62 | ||
|
|
16ef43b6ea | ||
|
|
e24c1bd400 | ||
|
|
6a3054d871 | ||
|
|
6bebed62b7 | ||
|
|
a7a114cac0 | ||
|
|
7ce5fae681 | ||
|
|
d1aaa49815 | ||
|
|
7a6e0bf87f | ||
|
|
a5cfb3abc7 | ||
|
|
f732328cf5 | ||
|
|
9fa37afab2 | ||
|
|
cc35f7db3e | ||
|
|
861b0646d6 | ||
|
|
cd4682e67e | ||
|
|
32baa11beb | ||
|
|
8ae04f79fd | ||
|
|
ddd1c8e9ff | ||
|
|
cb063893b2 | ||
|
|
8898f6707f | ||
|
|
bd8f16c5fd | ||
|
|
1431df9c9d | ||
|
|
5df1234892 | ||
|
|
0d24f5b428 | ||
|
|
55b70ae215 | ||
|
|
1ca3957ecb | ||
|
|
6be97bcb60 | ||
|
|
ac6ea3872c | ||
|
|
e33e3f89ac | ||
|
|
4bc4e0ed12 | ||
|
|
5b29c5989e | ||
|
|
2477612eef | ||
|
|
5df2c491cf | ||
|
|
0639ea8d17 | ||
|
|
19ebf81524 | ||
|
|
278a19a558 | ||
|
|
0b9ee9c3fd | ||
|
|
2da99428ae | ||
|
|
e5b02cb8d9 | ||
|
|
88c02e713d | ||
|
|
8a395ce8e5 | ||
|
|
8fb3e0ed54 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -34,7 +34,7 @@ frontend/src/constants/env.ts
|
||||
**/.vscode
|
||||
**/build
|
||||
**/storage
|
||||
**/locust-scripts/__pycache__/
|
||||
**/__pycache__/
|
||||
**/__debug_bin
|
||||
|
||||
.env
|
||||
|
||||
@@ -649,12 +649,12 @@
|
||||
|
||||
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
|
||||
-->
|
||||
<!--
|
||||
|
||||
<macros>
|
||||
<shard>01</shard>
|
||||
<replica>example01-01-1</replica>
|
||||
</macros>
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->
|
||||
|
||||
@@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.49.1
|
||||
image: signoz/query-service:0.53.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.48.0
|
||||
image: signoz/frontend:0.53.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -199,7 +199,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.102.2
|
||||
image: signoz/signoz-otel-collector:0.102.7
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -238,7 +238,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.102.2
|
||||
image: signoz/signoz-schema-migrator:0.102.7
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -649,12 +649,12 @@
|
||||
|
||||
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
|
||||
-->
|
||||
<!--
|
||||
|
||||
<macros>
|
||||
<shard>01</shard>
|
||||
<replica>example01-01-1</replica>
|
||||
</macros>
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -81,7 +81,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: signoz-otel-collector
|
||||
image: signoz/signoz-otel-collector:0.102.2
|
||||
image: signoz/signoz-otel-collector:0.102.7
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
|
||||
42
deploy/docker/clickhouse-setup/docker-compose-flask.yaml
Normal file
42
deploy/docker/clickhouse-setup/docker-compose-flask.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
version: "2.4"
|
||||
services:
|
||||
mongodb:
|
||||
image: "mongo:latest"
|
||||
container_name: mongodb
|
||||
hostname: mongodb
|
||||
restart: always
|
||||
# environment:
|
||||
# MONGO_INITDB_ROOT_USERNAME: root
|
||||
# MONGO_INITDB_ROOT_PASSWORD: example
|
||||
# ports:
|
||||
# - 27017:27017
|
||||
|
||||
sample-flask:
|
||||
image: "signoz/sample-flask-app:latest"
|
||||
container_name: sample-flask
|
||||
hostname: sample-flask
|
||||
restart: always
|
||||
ports:
|
||||
- 5002:5002
|
||||
extra_hosts:
|
||||
- signoz:host-gateway
|
||||
environment:
|
||||
MONGO_HOST: mongodb
|
||||
OTEL_RESOURCE_ATTRIBUTES: service.name=sample-flask
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: http://signoz:4317
|
||||
|
||||
load-flask:
|
||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
||||
container_name: load-flask
|
||||
hostname: load-flask
|
||||
restart: always
|
||||
environment:
|
||||
ATTACKED_HOST: http://sample-flask:5002
|
||||
LOCUST_MODE: standalone
|
||||
NO_PROXY: standalone
|
||||
TASK_DELAY_FROM: 45
|
||||
TASK_DELAY_TO: 60
|
||||
QUIET_MODE: "${QUIET_MODE:-false}"
|
||||
LOCUST_OPTS: "--headless -u 5 -r 5"
|
||||
volumes:
|
||||
- ../common/locust-flask:/locust
|
||||
@@ -164,7 +164,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:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.53.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -204,7 +204,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.53.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -216,7 +216,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -230,7 +230,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.7}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -164,7 +164,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:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.53.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.53.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -215,7 +215,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -229,7 +229,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.7}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
20
deploy/docker/common/locust-flask/locustfile.py
Normal file
20
deploy/docker/common/locust-flask/locustfile.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from locust import HttpUser, task, between
|
||||
from uuid import uuid4
|
||||
class UserTasks(HttpUser):
|
||||
wait_time = between(30, 60)
|
||||
|
||||
@task(1)
|
||||
def list(self):
|
||||
self.client.get("/list")
|
||||
|
||||
@task(1)
|
||||
def add_todo(self):
|
||||
self.client.post("/action", data={"name": "new-todo-"+str(uuid4()), "desc":"new desc", "date": "1990-04-10", "pr":"1"})
|
||||
|
||||
@task(1)
|
||||
def update(self):
|
||||
self.client.post("/action3", data={"_id":"626682d44bd2839cd80eb079", "name":"todo-"+str(uuid4()), "desc": "update desc", "date": "1990-04-11", "pr":"2"})
|
||||
|
||||
@task(1)
|
||||
def generate_error(self):
|
||||
self.client.get("/generate-error")
|
||||
@@ -494,7 +494,7 @@ fi
|
||||
|
||||
start_docker
|
||||
|
||||
# $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up -d --remove-orphans || true
|
||||
# $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml -f ./docker/clickhouse-setup/docker-compose-flask.yaml up -d --remove-orphans || true
|
||||
|
||||
|
||||
echo ""
|
||||
@@ -506,7 +506,7 @@ echo "🟡 Starting the SigNoz containers. It may take a few minutes ..."
|
||||
echo
|
||||
# The docker-compose command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the
|
||||
# script doesn't exit because this command looks like it failed to do it's thing.
|
||||
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up --detach --remove-orphans || true
|
||||
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml -f ./docker/clickhouse-setup/docker-compose-flask.yaml up --detach --remove-orphans || true
|
||||
|
||||
wait_for_containers_start 60
|
||||
echo ""
|
||||
|
||||
@@ -49,5 +49,6 @@
|
||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||
"SHORTCUTS": "SigNoz | Shortcuts",
|
||||
"INTEGRATIONS": "SigNoz | Integrations"
|
||||
"INTEGRATIONS": "SigNoz | Integrations",
|
||||
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
|
||||
}
|
||||
|
||||
@@ -204,3 +204,15 @@ export const InstalledIntegrations = Loadable(
|
||||
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
|
||||
),
|
||||
);
|
||||
|
||||
export const MessagingQueues = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "MessagingQueues" */ 'pages/MessagingQueues'),
|
||||
);
|
||||
|
||||
export const MQDetailPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
|
||||
),
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
LogsExplorer,
|
||||
LogsIndexToFields,
|
||||
LogsSaveViews,
|
||||
MessagingQueues,
|
||||
MQDetailPage,
|
||||
MySettings,
|
||||
NewDashboardPage,
|
||||
OldLogsExplorer,
|
||||
@@ -351,6 +353,20 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'INTEGRATIONS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.MESSAGING_QUEUES,
|
||||
exact: true,
|
||||
component: MessagingQueues,
|
||||
key: 'MESSAGING_QUEUES',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.MESSAGING_QUEUES_DETAIL,
|
||||
exact: true,
|
||||
component: MQDetailPage,
|
||||
key: 'MESSAGING_QUEUES_DETAIL',
|
||||
isPrivate: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORT_ROUTE: AppRoutes = {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import './LogDetails.styles.scss';
|
||||
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import { RadioChangeEvent } from 'antd/lib';
|
||||
import cx from 'classnames';
|
||||
@@ -10,8 +11,13 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
||||
import JSONView from 'container/LogDetailedView/JsonView';
|
||||
import Overview from 'container/LogDetailedView/Overview';
|
||||
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
|
||||
import {
|
||||
aggregateAttributesResourcesToString,
|
||||
removeEscapeCharacters,
|
||||
unescapeString,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import dompurify from 'dompurify';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -28,11 +34,14 @@ import { useMemo, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
import { LogDetailProps } from './LogDetail.interfaces';
|
||||
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
||||
|
||||
const convert = new Convert();
|
||||
|
||||
function LogDetail({
|
||||
log,
|
||||
onClose,
|
||||
@@ -90,6 +99,17 @@ function LogDetail({
|
||||
}
|
||||
};
|
||||
|
||||
const htmlBody = useMemo(
|
||||
() => ({
|
||||
__html: convert.toHtml(
|
||||
dompurify.sanitize(unescapeString(log?.body || ''), {
|
||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
[log?.body],
|
||||
);
|
||||
|
||||
const handleJSONCopy = (): void => {
|
||||
copyToClipboard(LogJsonData);
|
||||
notifications.success({
|
||||
@@ -127,8 +147,8 @@ function LogDetail({
|
||||
>
|
||||
<div className="log-detail-drawer__log">
|
||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||
<Tooltip title={log?.body} placement="left">
|
||||
<Typography.Text className="log-body">{log?.body}</Typography.Text>
|
||||
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
|
||||
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
|
||||
</Tooltip>
|
||||
|
||||
<div className="log-overflow-shadow"> </div>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.small {
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ReactNode, useCallback, useEffect } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
function CopyClipboardHOC({
|
||||
entityKey,
|
||||
textToCopy,
|
||||
children,
|
||||
}: CopyClipboardHOCProps): JSX.Element {
|
||||
@@ -11,11 +12,15 @@ function CopyClipboardHOC({
|
||||
const { notifications } = useNotifications();
|
||||
useEffect(() => {
|
||||
if (value.value) {
|
||||
const key = entityKey || '';
|
||||
|
||||
const notificationMessage = `${key} copied to clipboard`;
|
||||
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
message: notificationMessage,
|
||||
});
|
||||
}
|
||||
}, [value, notifications]);
|
||||
}, [value, notifications, entityKey]);
|
||||
|
||||
const onClick = useCallback((): void => {
|
||||
setCopy(textToCopy);
|
||||
@@ -34,6 +39,7 @@ function CopyClipboardHOC({
|
||||
}
|
||||
|
||||
interface CopyClipboardHOCProps {
|
||||
entityKey: string | undefined;
|
||||
textToCopy: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
@@ -56,7 +57,7 @@ function LogGeneralField({
|
||||
const html = useMemo(
|
||||
() => ({
|
||||
__html: convert.toHtml(
|
||||
dompurify.sanitize(fieldValue, {
|
||||
dompurify.sanitize(unescapeString(fieldValue), {
|
||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import Convert from 'ansi-to-html';
|
||||
import { DrawerProps } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
@@ -145,7 +146,9 @@ function RawLogView({
|
||||
const html = useMemo(
|
||||
() => ({
|
||||
__html: convert.toHtml(
|
||||
dompurify.sanitize(text, { FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS] }),
|
||||
dompurify.sanitize(unescapeString(text), {
|
||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
[text],
|
||||
|
||||
@@ -4,6 +4,7 @@ import Convert from 'ansi-to-html';
|
||||
import { Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import cx from 'classnames';
|
||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -115,7 +116,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
<TableBodyContent
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: convert.toHtml(
|
||||
dompurify.sanitize(field, {
|
||||
dompurify.sanitize(unescapeString(field), {
|
||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -32,4 +32,8 @@ export enum QueryParams {
|
||||
relativeTime = 'relativeTime',
|
||||
alertType = 'alertType',
|
||||
ruleId = 'ruleId',
|
||||
consumerGrp = 'consumerGrp',
|
||||
topic = 'topic',
|
||||
partition = 'partition',
|
||||
selectedTimelineQuery = 'selectedTimelineQuery',
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ export const REACT_QUERY_KEY = {
|
||||
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
|
||||
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
|
||||
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
|
||||
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
|
||||
};
|
||||
|
||||
@@ -54,6 +54,8 @@ const ROUTES = {
|
||||
WORKSPACE_LOCKED: '/workspace-locked',
|
||||
SHORTCUTS: '/shortcuts',
|
||||
INTEGRATIONS: '/integrations',
|
||||
MESSAGING_QUEUES: '/messaging-queues',
|
||||
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
||||
@@ -9,6 +9,7 @@ export const GlobalShortcuts = {
|
||||
NavigateToDashboards: 'd+shift',
|
||||
NavigateToAlerts: 'a+shift',
|
||||
NavigateToExceptions: 'e+shift',
|
||||
NavigateToMessagingQueues: 'm+shift',
|
||||
};
|
||||
|
||||
export const GlobalShortcutsName = {
|
||||
@@ -19,6 +20,7 @@ export const GlobalShortcutsName = {
|
||||
NavigateToDashboards: 'shift+d',
|
||||
NavigateToAlerts: 'shift+a',
|
||||
NavigateToExceptions: 'shift+e',
|
||||
NavigateToMessagingQueues: 'shift+m',
|
||||
};
|
||||
|
||||
export const GlobalShortcutsDescription = {
|
||||
@@ -29,4 +31,5 @@ export const GlobalShortcutsDescription = {
|
||||
NavigateToDashboards: 'Navigate to dashboards page',
|
||||
NavigateToAlerts: 'Navigate to alerts page',
|
||||
NavigateToExceptions: 'Navigate to Exceptions page',
|
||||
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
UPDATE_LATEST_VERSION_ERROR,
|
||||
} from 'types/actions/app';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||
|
||||
import { ChildrenContainer, Layout, LayoutContent } from './styles';
|
||||
@@ -71,7 +72,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const isPremiumChatSupportEnabled =
|
||||
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||
|
||||
const isChatSupportEnabled =
|
||||
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
|
||||
const showAddCreditCardModal =
|
||||
isChatSupportEnabled &&
|
||||
isCloudUserVal &&
|
||||
!isPremiumChatSupportEnabled &&
|
||||
!licenseData?.payload?.trialConvertedToSubscription;
|
||||
|
||||
@@ -241,6 +249,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const isTracesView = (): boolean =>
|
||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||
|
||||
const isMessagingQueues = (): boolean =>
|
||||
routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL';
|
||||
|
||||
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
||||
const isDashboardView = (): boolean => {
|
||||
/**
|
||||
@@ -329,7 +340,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isTracesView() ||
|
||||
isDashboardView() ||
|
||||
isDashboardWidgetView() ||
|
||||
isDashboardListView()
|
||||
isDashboardListView() ||
|
||||
isMessagingQueues()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
}}
|
||||
|
||||
@@ -47,6 +47,7 @@ function WidgetGraphComponent({
|
||||
setRequestData,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
customTooltipElement,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
@@ -335,6 +336,7 @@ function WidgetGraphComponent({
|
||||
onClickHandler={onClickHandler}
|
||||
onDragSelect={onDragSelect}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
customTooltipElement={customTooltipElement}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -33,6 +33,7 @@ function GridCardGraph({
|
||||
version,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
customTooltipElement,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
@@ -215,6 +216,7 @@ function GridCardGraph({
|
||||
setRequestData={setRequestData}
|
||||
onClickHandler={onClickHandler}
|
||||
onDragSelect={onDragSelect}
|
||||
customTooltipElement={customTooltipElement}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface WidgetGraphComponentProps {
|
||||
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
}
|
||||
|
||||
export interface GridCardGraphProps {
|
||||
@@ -42,6 +43,7 @@ export interface GridCardGraphProps {
|
||||
variables?: Dashboard['data']['variables'];
|
||||
version?: string;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 8;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import TableView from './TableView';
|
||||
import { removeEscapeCharacters } from './utils';
|
||||
|
||||
interface OverviewProps {
|
||||
logData: ILog;
|
||||
@@ -124,7 +125,7 @@ function Overview({
|
||||
children: (
|
||||
<div className="logs-body-content">
|
||||
<MEditor
|
||||
value={logData.body}
|
||||
value={removeEscapeCharacters(logData.body)}
|
||||
language="json"
|
||||
options={options}
|
||||
onChange={(): void => {}}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import './TableViewActions.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Button, Popover, Spin, Tooltip, Tree } from 'antd';
|
||||
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
|
||||
import cx from 'classnames';
|
||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import dompurify from 'dompurify';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||
|
||||
import { DataType } from '../TableView';
|
||||
import {
|
||||
@@ -19,6 +22,7 @@ import {
|
||||
jsonToDataNodes,
|
||||
recursiveParseJSON,
|
||||
removeEscapeCharacters,
|
||||
unescapeString,
|
||||
} from '../utils';
|
||||
|
||||
interface ITableViewActionsProps {
|
||||
@@ -39,6 +43,8 @@ interface ITableViewActionsProps {
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
const convert = new Convert();
|
||||
|
||||
export function TableViewActions(
|
||||
props: ITableViewActionsProps,
|
||||
): React.ReactElement {
|
||||
@@ -61,7 +67,7 @@ export function TableViewActions(
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const textToCopy = fieldData.value.slice(1, -1);
|
||||
const textToCopy = fieldData.value;
|
||||
|
||||
if (record.field === 'body') {
|
||||
const parsedBody = recursiveParseJSON(fieldData.value);
|
||||
@@ -71,22 +77,45 @@ export function TableViewActions(
|
||||
);
|
||||
}
|
||||
}
|
||||
const bodyHtml =
|
||||
record.field === 'body'
|
||||
? {
|
||||
__html: convert.toHtml(
|
||||
dompurify.sanitize(unescapeString(record.value), {
|
||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||
}),
|
||||
),
|
||||
}
|
||||
: { __html: '' };
|
||||
|
||||
const fieldFilterKey = filterKeyForField(fieldData.field);
|
||||
|
||||
return (
|
||||
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
|
||||
<CopyClipboardHOC textToCopy={textToCopy}>
|
||||
<span
|
||||
style={{
|
||||
color: Color.BG_SIENNA_400,
|
||||
whiteSpace: 'pre-wrap',
|
||||
tabSize: 4,
|
||||
}}
|
||||
>
|
||||
{removeEscapeCharacters(fieldData.value)}
|
||||
</span>
|
||||
</CopyClipboardHOC>
|
||||
{record.field === 'body' ? (
|
||||
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
|
||||
<span
|
||||
style={{
|
||||
color: Color.BG_SIENNA_400,
|
||||
whiteSpace: 'pre-wrap',
|
||||
tabSize: 4,
|
||||
}}
|
||||
dangerouslySetInnerHTML={bodyHtml}
|
||||
/>
|
||||
</CopyClipboardHOC>
|
||||
) : (
|
||||
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
|
||||
<span
|
||||
style={{
|
||||
color: Color.BG_SIENNA_400,
|
||||
whiteSpace: 'pre-wrap',
|
||||
tabSize: 4,
|
||||
}}
|
||||
>
|
||||
{removeEscapeCharacters(fieldData.value)}
|
||||
</span>
|
||||
</CopyClipboardHOC>
|
||||
)}
|
||||
|
||||
{!isListViewPanel && (
|
||||
<span className="action-btn">
|
||||
|
||||
@@ -250,19 +250,37 @@ export const getDataTypes = (value: unknown): DataTypes => {
|
||||
return determineType(value);
|
||||
};
|
||||
|
||||
// now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make
|
||||
// the log line readable
|
||||
export const removeEscapeCharacters = (str: string): string =>
|
||||
str.replace(/\\([ntfr'"\\])/g, (_: string, char: string) => {
|
||||
const escapeMap: Record<string, string> = {
|
||||
n: '\n',
|
||||
t: '\t',
|
||||
f: '\f',
|
||||
r: '\r',
|
||||
"'": "'",
|
||||
'"': '"',
|
||||
'\\': '\\',
|
||||
};
|
||||
return escapeMap[char as keyof typeof escapeMap];
|
||||
});
|
||||
str
|
||||
.replace(/\\x1[bB][[0-9;]*m/g, '')
|
||||
.replace(/\\u001[bB][[0-9;]*m/g, '')
|
||||
.replace(/\\x[0-9A-Fa-f]{2}/g, '')
|
||||
.replace(/\\u[0-9A-Fa-f]{4}/g, '')
|
||||
.replace(/\\[btnfrv0'"\\]/g, '');
|
||||
|
||||
// we need to remove the escape from the escaped characters as some recievers like file log escape the unicode escape characters.
|
||||
// example: Log [\u001B[32;1mThis is bright green\u001B[0m] is being sent as [\\u001B[32;1mThis is bright green\\u001B[0m]
|
||||
//
|
||||
// so we need to remove this escapes to render the color properly
|
||||
export const unescapeString = (str: string): string =>
|
||||
str
|
||||
.replace(/\\n/g, '\n') // Replaces escaped newlines
|
||||
.replace(/\\r/g, '\r') // Replaces escaped carriage returns
|
||||
.replace(/\\t/g, '\t') // Replaces escaped tabs
|
||||
.replace(/\\b/g, '\b') // Replaces escaped backspaces
|
||||
.replace(/\\f/g, '\f') // Replaces escaped form feeds
|
||||
.replace(/\\v/g, '\v') // Replaces escaped vertical tabs
|
||||
.replace(/\\'/g, "'") // Replaces escaped single quotes
|
||||
.replace(/\\"/g, '"') // Replaces escaped double quotes
|
||||
.replace(/\\\\/g, '\\') // Replaces escaped backslashes
|
||||
.replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) =>
|
||||
String.fromCharCode(parseInt(hex, 16)),
|
||||
) // Replaces hexadecimal escape sequences
|
||||
.replace(/\\u([0-9A-Fa-f]{4})/g, (_, hex) =>
|
||||
String.fromCharCode(parseInt(hex, 16)),
|
||||
); // Replaces Unicode escape sequences
|
||||
|
||||
export function removeExtraSpaces(input: string): string {
|
||||
return input.replace(/\s+/g, ' ').trim();
|
||||
|
||||
@@ -155,6 +155,7 @@ function LogsExplorerList({
|
||||
>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
key={activeLogIndex || 'logs-virtuoso'}
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
|
||||
@@ -100,14 +100,14 @@ function LogsExplorerViews({
|
||||
// this is to respect the panel type present in the URL rather than defaulting it to list always.
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
|
||||
const { activeLogId, onTimeRangeChange } = useCopyLogLink();
|
||||
|
||||
const { queryData: pageSize } = useUrlQueryData(
|
||||
QueryParams.pageSize,
|
||||
DEFAULT_PER_PAGE_VALUE,
|
||||
);
|
||||
|
||||
const { minTime } = useSelector<AppState, GlobalReducer>(
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
@@ -254,11 +254,10 @@ function LogsExplorerViews({
|
||||
enabled: !isLimit && !!requestData,
|
||||
},
|
||||
{
|
||||
...(timeRange &&
|
||||
activeLogId &&
|
||||
...(activeLogId &&
|
||||
!logs.length && {
|
||||
start: timeRange.start,
|
||||
end: timeRange.end,
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
}),
|
||||
},
|
||||
undefined,
|
||||
@@ -521,7 +520,7 @@ function LogsExplorerViews({
|
||||
setLogs(newLogs);
|
||||
onTimeRangeChange({
|
||||
start: currentParams?.start,
|
||||
end: timeRange?.end || currentParams?.end,
|
||||
end: currentParams?.end,
|
||||
pageSize: newLogs.length,
|
||||
});
|
||||
}
|
||||
@@ -538,8 +537,7 @@ function LogsExplorerViews({
|
||||
filters: listQuery?.filters || initialFilters,
|
||||
page: 1,
|
||||
log: null,
|
||||
pageSize:
|
||||
timeRange?.pageSize && activeLogId ? timeRange?.pageSize : pageSize,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
setLogs([]);
|
||||
@@ -554,7 +552,6 @@ function LogsExplorerViews({
|
||||
listQuery,
|
||||
pageSize,
|
||||
minTime,
|
||||
timeRange,
|
||||
activeLogId,
|
||||
onTimeRangeChange,
|
||||
panelType,
|
||||
|
||||
@@ -10,12 +10,14 @@ import LogsTableView from 'components/Logs/TableView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
// interfaces
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
@@ -57,6 +59,14 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
liveTail,
|
||||
]);
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
// this component will alwyays be called on old logs explorer page itself!
|
||||
dataSource: DataSource.LOGS,
|
||||
// and we do not have table / timeseries aggregated views in the old logs explorer!
|
||||
aggregateOperator: StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(index: number): JSX.Element => {
|
||||
const log = logs[index];
|
||||
@@ -68,7 +78,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
data={log}
|
||||
linesPerRow={linesPerRow}
|
||||
selectedFields={selected}
|
||||
fontSize={FontSize.SMALL}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -81,11 +91,19 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
linesPerRow={linesPerRow}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onSetActiveLog={onSetActiveLog}
|
||||
fontSize={FontSize.SMALL}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[logs, viewMode, selected, onAddToQuery, onSetActiveLog, linesPerRow],
|
||||
[
|
||||
logs,
|
||||
viewMode,
|
||||
selected,
|
||||
linesPerRow,
|
||||
onAddToQuery,
|
||||
onSetActiveLog,
|
||||
options.fontSize,
|
||||
],
|
||||
);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
@@ -96,7 +114,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
logs={logs}
|
||||
fields={selected}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={FontSize.SMALL}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -108,7 +126,15 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
);
|
||||
}, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]);
|
||||
}, [
|
||||
getItemContent,
|
||||
linesPerRow,
|
||||
logs,
|
||||
onSetActiveLog,
|
||||
options.fontSize,
|
||||
selected,
|
||||
viewMode,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner height={20} tip="Getting Logs" />;
|
||||
|
||||
@@ -26,7 +26,9 @@ function MySettings(): JSX.Element {
|
||||
label: (
|
||||
<div className="theme-option">
|
||||
<Sun size={12} data-testid="light-theme-icon" /> Light{' '}
|
||||
<Tag color="magenta">Beta</Tag>
|
||||
<Tag bordered={false} color="geekblue">
|
||||
Beta
|
||||
</Tag>
|
||||
</div>
|
||||
),
|
||||
value: 'light',
|
||||
|
||||
@@ -247,8 +247,7 @@ export default function Onboarding(): JSX.Element {
|
||||
|
||||
const handleNext = (): void => {
|
||||
if (activeStep <= 3) {
|
||||
handleNextStep();
|
||||
history.replace(moduleRouteMap[selectedModule.id as ModulesMap]);
|
||||
history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -258,6 +257,13 @@ export default function Onboarding(): JSX.Element {
|
||||
updateSelectedDataSource(null);
|
||||
};
|
||||
|
||||
const handleBackNavigation = (): void => {
|
||||
setCurrent(0);
|
||||
setActiveStep(1);
|
||||
setSelectedModule(useCases.APM);
|
||||
resetProgress();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { pathname } = location;
|
||||
|
||||
@@ -277,9 +283,11 @@ export default function Onboarding(): JSX.Element {
|
||||
} else if (pathname === ROUTES.GET_STARTED_AZURE_MONITORING) {
|
||||
handleModuleSelect(useCases.AzureMonitoring);
|
||||
handleNextStep();
|
||||
} else {
|
||||
handleBackNavigation();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [location.pathname]);
|
||||
|
||||
const [form] = Form.useForm<InviteMemberFormValues>();
|
||||
const [
|
||||
|
||||
@@ -15,6 +15,7 @@ function PanelWrapper({
|
||||
onDragSelect,
|
||||
selectedGraph,
|
||||
tableProcessedDataRef,
|
||||
customTooltipElement,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const Component = PanelTypeVsPanelWrapper[
|
||||
selectedGraph || widget.panelTypes
|
||||
@@ -37,6 +38,7 @@ function PanelWrapper({
|
||||
onDragSelect={onDragSelect}
|
||||
selectedGraph={selectedGraph}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
customTooltipElement={customTooltipElement}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ function UplotPanelWrapper({
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
selectedGraph,
|
||||
customTooltipElement,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -126,6 +127,7 @@ function UplotPanelWrapper({
|
||||
stackBarChart: widget?.stackedBarChart,
|
||||
hiddenGraph,
|
||||
setHiddenGraph,
|
||||
customTooltipElement,
|
||||
}),
|
||||
[
|
||||
widget?.id,
|
||||
@@ -147,6 +149,7 @@ function UplotPanelWrapper({
|
||||
selectedGraph,
|
||||
currentQuery,
|
||||
hiddenGraph,
|
||||
customTooltipElement,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export type PanelWrapperProps = {
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
selectedGraph?: PANEL_TYPES;
|
||||
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
};
|
||||
|
||||
export type TooltipData = {
|
||||
|
||||
@@ -62,7 +62,9 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
dataSource: query.dataSource,
|
||||
}),
|
||||
{
|
||||
enabled: !!query.aggregateOperator && !!query.dataSource,
|
||||
enabled:
|
||||
query.dataSource === DataSource.METRICS ||
|
||||
(!!query.aggregateOperator && !!query.dataSource),
|
||||
onSuccess: (data) => {
|
||||
const options: ExtendedSelectOption[] =
|
||||
data?.payload?.attributeKeys?.map(({ id: _, ...item }) => ({
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.beta-tag {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
|
||||
@@ -24,14 +24,16 @@ export default function NavItem({
|
||||
onClick={(event): void => onClick(event)}
|
||||
>
|
||||
<div className="nav-item-active-marker" />
|
||||
<div className="nav-item-data">
|
||||
<div className={cx('nav-item-data', isBeta ? 'beta-tag' : '')}>
|
||||
<div className="nav-item-icon">{icon}</div>
|
||||
|
||||
<div className="nav-item-label">{label}</div>
|
||||
|
||||
{isBeta && (
|
||||
<div className="nav-item-beta">
|
||||
<Tag color="magenta">Beta</Tag>
|
||||
<Tag bordered={false} color="geekblue">
|
||||
Beta
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -347,6 +347,10 @@ function SideNav({
|
||||
onClickHandler(ROUTES.ALL_DASHBOARD, null),
|
||||
);
|
||||
|
||||
registerShortcut(GlobalShortcuts.NavigateToMessagingQueues, () =>
|
||||
onClickHandler(ROUTES.MESSAGING_QUEUES, null),
|
||||
);
|
||||
|
||||
registerShortcut(GlobalShortcuts.NavigateToAlerts, () =>
|
||||
onClickHandler(ROUTES.LIST_ALL_ALERT, null),
|
||||
);
|
||||
@@ -362,6 +366,7 @@ function SideNav({
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToDashboards);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToAlerts);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToExceptions);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToMessagingQueues);
|
||||
};
|
||||
}, [deregisterShortcut, onClickHandler, onCollapse, registerShortcut]);
|
||||
|
||||
|
||||
@@ -48,4 +48,6 @@ export const routeConfig: Record<string, QueryParams[]> = {
|
||||
[ROUTES.TRACE_EXPLORER]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.LOGS_PIPELINES]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes],
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
FileKey2,
|
||||
Layers2,
|
||||
LayoutGrid,
|
||||
ListMinus,
|
||||
MessageSquare,
|
||||
Receipt,
|
||||
Route,
|
||||
@@ -86,6 +87,12 @@ const menuItems: SidebarItem[] = [
|
||||
label: 'Dashboards',
|
||||
icon: <LayoutGrid size={16} />,
|
||||
},
|
||||
{
|
||||
key: ROUTES.MESSAGING_QUEUES,
|
||||
label: 'Messaging Queues',
|
||||
icon: <ListMinus size={16} />,
|
||||
isBeta: true,
|
||||
},
|
||||
{
|
||||
key: ROUTES.LIST_ALL_ALERT,
|
||||
label: 'Alerts',
|
||||
|
||||
@@ -27,6 +27,7 @@ const breadcrumbNameMap: Record<string, string> = {
|
||||
[ROUTES.BILLING]: 'Billing',
|
||||
[ROUTES.SUPPORT]: 'Support',
|
||||
[ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked',
|
||||
[ROUTES.MESSAGING_QUEUES]: 'Messaging Queues',
|
||||
};
|
||||
|
||||
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {
|
||||
|
||||
@@ -208,6 +208,8 @@ export const routesToSkip = [
|
||||
ROUTES.DASHBOARD,
|
||||
ROUTES.DASHBOARD_WIDGET,
|
||||
ROUTES.SERVICE_TOP_LEVEL_OPERATIONS,
|
||||
ROUTES.MESSAGING_QUEUES,
|
||||
ROUTES.MESSAGING_QUEUES_DETAIL,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
||||
@@ -468,7 +468,6 @@ function DateTimeSelection({
|
||||
if (updatedTime !== 'custom') {
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, updatedTime);
|
||||
} else {
|
||||
const startTime = preStartTime.toString();
|
||||
@@ -476,6 +475,7 @@ function DateTimeSelection({
|
||||
|
||||
urlQuery.set(QueryParams.startTime, startTime);
|
||||
urlQuery.set(QueryParams.endTime, endTime);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
}
|
||||
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
|
||||
@@ -219,10 +219,18 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
<Col flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`} />
|
||||
<Col flex="auto">
|
||||
<StyledSpace styledclass={[styles.floatRight]}>
|
||||
<Button onClick={onFocusSelectedSpanHandler} icon={<FilterOutlined />}>
|
||||
<Button
|
||||
onClick={onFocusSelectedSpanHandler}
|
||||
icon={<FilterOutlined />}
|
||||
data-testid="span-focus-btn"
|
||||
>
|
||||
Focus on selected span
|
||||
</Button>
|
||||
<Button type="default" onClick={onResetHandler}>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={onResetHandler}
|
||||
data-testid="reset-focus"
|
||||
>
|
||||
Reset Focus
|
||||
</Button>
|
||||
</StyledSpace>
|
||||
@@ -262,6 +270,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
collapsedWidth={40}
|
||||
defaultCollapsed
|
||||
onCollapse={(value): void => setCollapsed(value)}
|
||||
data-testid="span-details-sider"
|
||||
>
|
||||
{!collapsed && (
|
||||
<StyledCol styledclass={[styles.selectedSpanDetailContainer]}>
|
||||
|
||||
@@ -12,7 +12,6 @@ export type UseCopyLogLink = {
|
||||
isHighlighted: boolean;
|
||||
isLogsExplorerPage: boolean;
|
||||
activeLogId: string | null;
|
||||
timeRange: LogTimeRange | null;
|
||||
onLogCopy: MouseEventHandler<HTMLElement>;
|
||||
onTimeRangeChange: (newTimeRange: LogTimeRange | null) => void;
|
||||
};
|
||||
|
||||
@@ -26,33 +26,25 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { queryData: timeRange } = useUrlQueryData<LogTimeRange | null>(
|
||||
QueryParams.timeRange,
|
||||
null,
|
||||
);
|
||||
|
||||
const { queryData: activeLogId } = useUrlQueryData<string | null>(
|
||||
QueryParams.activeLogId,
|
||||
null,
|
||||
);
|
||||
|
||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { selectedTime, minTime, maxTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const onTimeRangeChange = useCallback(
|
||||
(newTimeRange: LogTimeRange | null): void => {
|
||||
urlQuery.set(QueryParams.timeRange, JSON.stringify(newTimeRange));
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, selectedTime);
|
||||
} else {
|
||||
urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || '');
|
||||
urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || '');
|
||||
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
}
|
||||
|
||||
@@ -76,14 +68,12 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const range = JSON.stringify(timeRange);
|
||||
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
urlQuery.delete(QueryParams.timeRange);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
|
||||
urlQuery.set(QueryParams.activeLogId, `"${logId}"`);
|
||||
urlQuery.set(QueryParams.timeRange, range);
|
||||
urlQuery.set(QueryParams.startTime, timeRange?.start.toString() || '');
|
||||
urlQuery.set(QueryParams.endTime, timeRange?.end.toString() || '');
|
||||
urlQuery.set(QueryParams.startTime, minTime?.toString() || '');
|
||||
urlQuery.set(QueryParams.endTime, maxTime?.toString() || '');
|
||||
|
||||
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
|
||||
|
||||
@@ -92,7 +82,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
},
|
||||
[logId, timeRange, urlQuery, pathname, setCopy, notifications],
|
||||
[logId, urlQuery, minTime, maxTime, pathname, setCopy, notifications],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -110,7 +100,6 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
isHighlighted,
|
||||
isLogsExplorerPage,
|
||||
activeLogId,
|
||||
timeRange,
|
||||
onLogCopy,
|
||||
onTimeRangeChange,
|
||||
};
|
||||
|
||||
@@ -92,15 +92,9 @@ export const useFetchKeysAndValues = (
|
||||
const isQueryEnabled = useMemo(
|
||||
() =>
|
||||
query.dataSource === DataSource.METRICS
|
||||
? !!query.aggregateOperator &&
|
||||
!!query.dataSource &&
|
||||
!!query.aggregateAttribute.dataType
|
||||
? !!query.dataSource && !!query.aggregateAttribute.dataType
|
||||
: true,
|
||||
[
|
||||
query.aggregateAttribute.dataType,
|
||||
query.aggregateOperator,
|
||||
query.dataSource,
|
||||
],
|
||||
[query.aggregateAttribute.dataType, query.dataSource],
|
||||
);
|
||||
|
||||
const { data, isFetching, status } = useGetAggregateKeys(
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { LogTimeRange } from './logs/types';
|
||||
import { useCopyLogLink } from './logs/useCopyLogLink';
|
||||
@@ -39,6 +42,10 @@ export const useLogsData = ({
|
||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||
const [shouldLoadMoreLogs, setShouldLoadMoreLogs] = useState<boolean>(false);
|
||||
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const { queryData: pageSize } = useUrlQueryData(
|
||||
QueryParams.pageSize,
|
||||
DEFAULT_PER_PAGE_VALUE,
|
||||
@@ -122,7 +129,7 @@ export const useLogsData = ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
|
||||
const { activeLogId, onTimeRangeChange } = useCopyLogLink();
|
||||
|
||||
const { data, isFetching } = useGetExplorerQueryRange(
|
||||
requestData,
|
||||
@@ -133,11 +140,10 @@ export const useLogsData = ({
|
||||
enabled: !isLimit && !!requestData,
|
||||
},
|
||||
{
|
||||
...(timeRange &&
|
||||
activeLogId &&
|
||||
...(activeLogId &&
|
||||
!logs.length && {
|
||||
start: timeRange.start,
|
||||
end: timeRange.end,
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
}),
|
||||
},
|
||||
shouldLoadMoreLogs,
|
||||
@@ -156,7 +162,7 @@ export const useLogsData = ({
|
||||
setLogs(newLogs);
|
||||
onTimeRangeChange({
|
||||
start: currentParams?.start,
|
||||
end: timeRange?.end || currentParams?.end,
|
||||
end: currentParams?.end,
|
||||
pageSize: newLogs.length,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface GetUPlotChartOptions {
|
||||
[key: string]: boolean;
|
||||
}>
|
||||
>;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
}
|
||||
|
||||
/** the function converts series A , series B , series C to
|
||||
@@ -154,6 +155,7 @@ export const getUPlotChartOptions = ({
|
||||
stackBarChart: stackChart,
|
||||
hiddenGraph,
|
||||
setHiddenGraph,
|
||||
customTooltipElement,
|
||||
}: GetUPlotChartOptions): uPlot.Options => {
|
||||
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
||||
|
||||
@@ -209,9 +211,16 @@ export const getUPlotChartOptions = ({
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tooltipPlugin({ apiResponse, yAxisUnit, stackBarChart, isDarkMode }),
|
||||
tooltipPlugin({
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
stackBarChart,
|
||||
isDarkMode,
|
||||
customTooltipElement,
|
||||
}),
|
||||
onClickPlugin({
|
||||
onClick: onClickHandler,
|
||||
apiResponse,
|
||||
}),
|
||||
],
|
||||
hooks: {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
export interface OnClickPluginOpts {
|
||||
onClick: (
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
data?: {
|
||||
[key: string]: string;
|
||||
},
|
||||
) => void;
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
}
|
||||
|
||||
function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
|
||||
@@ -22,9 +28,24 @@ function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
|
||||
const xValue = u.posToVal(event.offsetX, 'x');
|
||||
const yValue = u.posToVal(event.offsetY, 'y');
|
||||
|
||||
opts.onClick(xValue, yValue, mouseX, mouseY);
|
||||
};
|
||||
let metric = {};
|
||||
const { series } = u;
|
||||
const apiResult = opts.apiResponse?.data?.result || [];
|
||||
|
||||
// this is to get the metric value of the focused series
|
||||
if (Array.isArray(series) && series.length > 0) {
|
||||
series.forEach((item, index) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (item?.show && item?._focus) {
|
||||
const { metric: focusedMetric } = apiResult[index - 1] || [];
|
||||
metric = focusedMetric;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
opts.onClick(xValue, yValue, mouseX, mouseY, metric);
|
||||
};
|
||||
u.over.addEventListener('click', handleClick);
|
||||
},
|
||||
destroy: (u: uPlot) => {
|
||||
|
||||
@@ -222,6 +222,7 @@ type ToolTipPluginProps = {
|
||||
isMergedSeries?: boolean;
|
||||
stackBarChart?: boolean;
|
||||
isDarkMode: boolean;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
};
|
||||
|
||||
const tooltipPlugin = ({
|
||||
@@ -232,7 +233,9 @@ const tooltipPlugin = ({
|
||||
isMergedSeries,
|
||||
stackBarChart,
|
||||
isDarkMode,
|
||||
}: ToolTipPluginProps): any => {
|
||||
customTooltipElement,
|
||||
}: // eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
ToolTipPluginProps): any => {
|
||||
let over: HTMLElement;
|
||||
let bound: HTMLElement;
|
||||
let bLeft: any;
|
||||
@@ -298,6 +301,9 @@ const tooltipPlugin = ({
|
||||
isMergedSeries,
|
||||
stackBarChart,
|
||||
);
|
||||
if (customTooltipElement) {
|
||||
content.appendChild(customTooltipElement);
|
||||
}
|
||||
overlay.appendChild(content);
|
||||
placement(overlay, anchor, 'right', 'start', { bound });
|
||||
}
|
||||
|
||||
@@ -78,13 +78,13 @@ export const explorerView = {
|
||||
extraData: '{"color":"#00ffd0"}',
|
||||
},
|
||||
{
|
||||
uuid: '58b010b6-8be9-40d1-8d25-f73b5f7314ad',
|
||||
name: 'success traces list view',
|
||||
uuid: '8c4bf492-d54d-4ab2-a8d6-9c1563f46e1f',
|
||||
name: 'R-test panel',
|
||||
category: '',
|
||||
createdAt: '2023-08-30T13:00:40.958011925Z',
|
||||
createdBy: 'test-email',
|
||||
updatedAt: '2024-04-29T13:09:06.175537361Z',
|
||||
updatedBy: 'test-email',
|
||||
createdAt: '2024-07-01T13:45:57.924686766Z',
|
||||
createdBy: 'test-user-test',
|
||||
updatedAt: '2024-07-01T13:48:31.032106578Z',
|
||||
updatedBy: 'test-user-test',
|
||||
sourcePage: 'traces',
|
||||
tags: [''],
|
||||
compositeQuery: {
|
||||
@@ -106,13 +106,13 @@ export const explorerView = {
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
key: 'responseStatusCode',
|
||||
key: 'httpMethod',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
value: '200',
|
||||
value: 'GET',
|
||||
op: '=',
|
||||
},
|
||||
],
|
||||
@@ -128,7 +128,7 @@ export const explorerView = {
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
reduceTo: 'sum',
|
||||
reduceTo: 'avg',
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
ShiftBy: 0,
|
||||
@@ -137,7 +137,7 @@ export const explorerView = {
|
||||
panelType: 'list',
|
||||
queryType: 'builder',
|
||||
},
|
||||
extraData: '{"color":"#bdff9d"}',
|
||||
extraData: '{"color":"#AD7F58"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
2089
frontend/src/mocks-server/__mockdata__/tracedetail.ts
Normal file
2089
frontend/src/mocks-server/__mockdata__/tracedetail.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import { membersResponse } from './__mockdata__/members';
|
||||
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
|
||||
import { serviceSuccessResponse } from './__mockdata__/services';
|
||||
import { topLevelOperationSuccessResponse } from './__mockdata__/top_level_operations';
|
||||
import { traceDetailResponse } from './__mockdata__/tracedetail';
|
||||
|
||||
export const handlers = [
|
||||
rest.post('http://localhost/api/v3/query_range', (req, res, ctx) =>
|
||||
@@ -230,6 +231,12 @@ export const handlers = [
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
rest.get(
|
||||
'http://localhost/api/v1/traces/000000000000000071dc9b0a338729b4',
|
||||
(req, res, ctx) => res(ctx.status(200), ctx.json(traceDetailResponse)),
|
||||
),
|
||||
|
||||
rest.post('http://localhost/api/v1//channels', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(allAlertChannels)),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.coming-soon {
|
||||
display: inline-flex;
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(173, 127, 88, 0.2);
|
||||
background: rgba(173, 127, 88, 0.1);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
&__text {
|
||||
color: var(--text-sienna-400);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.05px;
|
||||
line-height: normal;
|
||||
}
|
||||
&__icon {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-overlay {
|
||||
text-wrap: nowrap;
|
||||
.ant-tooltip-inner {
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.select-label-with-coming-soon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
58
frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx
Normal file
58
frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import './MQCommon.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip } from 'antd';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
export function ComingSoon(): JSX.Element {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
Join our Slack community for more details:{' '}
|
||||
<a
|
||||
href="https://signoz.io/slack"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
>
|
||||
SigNoz Community
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
overlayClassName="tooltip-overlay"
|
||||
>
|
||||
<div className="coming-soon">
|
||||
<div className="coming-soon__text">Coming Soon</div>
|
||||
<div className="coming-soon__icon">
|
||||
<Info size={10} color={Color.BG_SIENNA_400} />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function SelectMaxTagPlaceholder(
|
||||
omittedValues: Partial<DefaultOptionType>[],
|
||||
): JSX.Element {
|
||||
return (
|
||||
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>
|
||||
<span>+ {omittedValues.length} </span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function SelectLabelWithComingSoon({
|
||||
label,
|
||||
}: {
|
||||
label: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="select-label-with-coming-soon">
|
||||
{label} <ComingSoon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import '../MessagingQueues.styles.scss';
|
||||
|
||||
import { Select, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { ListMinus } from 'lucide-react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { MessagingQueuesViewType } from '../MessagingQueuesUtils';
|
||||
import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon';
|
||||
import MessagingQueuesDetails from '../MQDetails/MQDetails';
|
||||
import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions';
|
||||
import MessagingQueuesGraph from '../MQGraph/MQGraph';
|
||||
|
||||
function MQDetailPage(): JSX.Element {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<div className="messaging-queue-container">
|
||||
<div className="messaging-breadcrumb">
|
||||
<ListMinus size={16} />
|
||||
<Typography.Text
|
||||
onClick={(): void => history.push(ROUTES.MESSAGING_QUEUES)}
|
||||
className="message-queue-text"
|
||||
>
|
||||
Messaging Queues
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="messaging-header">
|
||||
<div className="header-config">
|
||||
Kafka / views /
|
||||
<Select
|
||||
className="messaging-queue-options"
|
||||
defaultValue={MessagingQueuesViewType.consumerLag.value}
|
||||
popupClassName="messaging-queue-options-popup"
|
||||
options={[
|
||||
{
|
||||
label: MessagingQueuesViewType.consumerLag.label,
|
||||
value: MessagingQueuesViewType.consumerLag.value,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<SelectLabelWithComingSoon
|
||||
label={MessagingQueuesViewType.partitionLatency.label}
|
||||
/>
|
||||
),
|
||||
value: MessagingQueuesViewType.partitionLatency.value,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<SelectLabelWithComingSoon
|
||||
label={MessagingQueuesViewType.producerLatency.label}
|
||||
/>
|
||||
),
|
||||
value: MessagingQueuesViewType.producerLatency.value,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<SelectLabelWithComingSoon
|
||||
label={MessagingQueuesViewType.consumerLatency.label}
|
||||
/>
|
||||
),
|
||||
value: MessagingQueuesViewType.consumerLatency.value,
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
|
||||
</div>
|
||||
<div className="messaging-queue-main-graph">
|
||||
<MessagingQueuesConfigOptions />
|
||||
<MessagingQueuesGraph />
|
||||
</div>
|
||||
<div className="messaging-queue-details">
|
||||
<MessagingQueuesDetails />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MQDetailPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import MQDetailPage from './MQDetailPage';
|
||||
|
||||
export default MQDetailPage;
|
||||
@@ -0,0 +1,6 @@
|
||||
.mq-details {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
67
frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx
Normal file
67
frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import './MQDetails.style.scss';
|
||||
|
||||
import { Radio } from 'antd';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
|
||||
import {
|
||||
ConsumerLagDetailTitle,
|
||||
ConsumerLagDetailType,
|
||||
} from '../MessagingQueuesUtils';
|
||||
import { ComingSoon } from '../MQCommon/MQCommon';
|
||||
import MessagingQueuesTable from './MQTables/MQTables';
|
||||
|
||||
function MessagingQueuesOptions({
|
||||
currentTab,
|
||||
setCurrentTab,
|
||||
}: {
|
||||
currentTab: ConsumerLagDetailType;
|
||||
setCurrentTab: Dispatch<SetStateAction<ConsumerLagDetailType>>;
|
||||
}): JSX.Element {
|
||||
const [option, setOption] = useState<ConsumerLagDetailType>(currentTab);
|
||||
|
||||
return (
|
||||
<Radio.Group
|
||||
onChange={(value): void => {
|
||||
setOption(value.target.value);
|
||||
setCurrentTab(value.target.value);
|
||||
}}
|
||||
value={option}
|
||||
className="mq-details-options"
|
||||
>
|
||||
<Radio.Button value={ConsumerLagDetailType.ConsumerDetails} checked>
|
||||
{ConsumerLagDetailTitle[ConsumerLagDetailType.ConsumerDetails]}
|
||||
</Radio.Button>
|
||||
<Radio.Button value={ConsumerLagDetailType.ProducerDetails}>
|
||||
{ConsumerLagDetailTitle[ConsumerLagDetailType.ProducerDetails]}
|
||||
</Radio.Button>
|
||||
<Radio.Button value={ConsumerLagDetailType.NetworkLatency}>
|
||||
{ConsumerLagDetailTitle[ConsumerLagDetailType.NetworkLatency]}
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
value={ConsumerLagDetailType.PartitionHostMetrics}
|
||||
disabled
|
||||
className="disabled-option"
|
||||
>
|
||||
{ConsumerLagDetailTitle[ConsumerLagDetailType.PartitionHostMetrics]}
|
||||
<ComingSoon />
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
);
|
||||
}
|
||||
|
||||
function MessagingQueuesDetails(): JSX.Element {
|
||||
const [currentTab, setCurrentTab] = useState<ConsumerLagDetailType>(
|
||||
ConsumerLagDetailType.ConsumerDetails,
|
||||
);
|
||||
return (
|
||||
<div className="mq-details">
|
||||
<MessagingQueuesOptions
|
||||
currentTab={currentTab}
|
||||
setCurrentTab={setCurrentTab}
|
||||
/>
|
||||
<MessagingQueuesTable currentTab={currentTab} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessagingQueuesDetails;
|
||||
@@ -0,0 +1,99 @@
|
||||
.mq-tables-container {
|
||||
.mq-table-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: Inter;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
letter-spacing: -0.09px;
|
||||
|
||||
.mq-table-subtitle {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.mq-table {
|
||||
width: 100%;
|
||||
|
||||
.ant-table-content {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
.ant-table-cell {
|
||||
max-width: 250px;
|
||||
|
||||
background-color: var(--bg-ink-400);
|
||||
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead {
|
||||
.ant-table-cell {
|
||||
background-color: var(--bg-ink-500);
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-style {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 6px;
|
||||
|
||||
.ant-typography {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.mq-tables-container {
|
||||
.mq-table-title {
|
||||
color: var(--bg-slate-200);
|
||||
|
||||
.mq-table-subtitle {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
|
||||
.mq-table {
|
||||
.ant-table-content {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
.ant-table-cell {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead {
|
||||
.ant-table-cell {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-style {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import './MQTables.styles.scss';
|
||||
|
||||
import { Skeleton, Table, Typography } from 'antd';
|
||||
import axios from 'axios';
|
||||
import { isNumber } from 'chart.js/helpers';
|
||||
import { ColumnTypeRender } from 'components/Logs/TableView/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { History } from 'history';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import {
|
||||
ConsumerLagDetailTitle,
|
||||
ConsumerLagDetailType,
|
||||
convertToTitleCase,
|
||||
RowData,
|
||||
SelectedTimelineQuery,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
ConsumerLagPayload,
|
||||
getConsumerLagDetails,
|
||||
MessagingQueuesPayloadProps,
|
||||
} from './getConsumerLagDetails';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function getColumns(
|
||||
data: MessagingQueuesPayloadProps['payload'],
|
||||
history: History<unknown>,
|
||||
): RowData[] {
|
||||
console.log(data);
|
||||
if (data?.result?.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const columns: {
|
||||
title: string;
|
||||
dataIndex: string;
|
||||
key: string;
|
||||
}[] = data?.result?.[0]?.table?.columns.map((column) => ({
|
||||
title: convertToTitleCase(column.name),
|
||||
dataIndex: column.name,
|
||||
key: column.name,
|
||||
render: [
|
||||
'p99',
|
||||
'error_rate',
|
||||
'throughput',
|
||||
'avg_msg_size',
|
||||
'error_percentage',
|
||||
].includes(column.name)
|
||||
? (value: number | string): string => {
|
||||
if (!isNumber(value)) return value.toString();
|
||||
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
|
||||
}
|
||||
: (text: string): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
children:
|
||||
column.name === 'service_name' ? (
|
||||
<Typography.Link
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
history.push(`/services/${encodeURIComponent(text)}`);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography.Link>
|
||||
) : (
|
||||
<Typography.Text>{text}</Typography.Text>
|
||||
),
|
||||
}),
|
||||
}));
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
export function getTableData(
|
||||
data: MessagingQueuesPayloadProps['payload'],
|
||||
): RowData[] {
|
||||
if (data?.result?.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tableData: RowData[] =
|
||||
data?.result?.[0]?.table?.rows?.map(
|
||||
(row, index: number): RowData => ({
|
||||
...row.data,
|
||||
key: index,
|
||||
}),
|
||||
) || [];
|
||||
|
||||
return tableData;
|
||||
}
|
||||
|
||||
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
|
||||
<>
|
||||
<Typography.Text className="numbers">
|
||||
{range[0]} — {range[1]}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="total"> of {total}</Typography.Text>
|
||||
</>
|
||||
);
|
||||
|
||||
function MessagingQueuesTable({
|
||||
currentTab,
|
||||
}: {
|
||||
currentTab: ConsumerLagDetailType;
|
||||
}): JSX.Element {
|
||||
const [columns, setColumns] = useState<any[]>([]);
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const { notifications } = useNotifications();
|
||||
const urlQuery = useUrlQuery();
|
||||
const history = useHistory();
|
||||
const timelineQuery = decodeURIComponent(
|
||||
urlQuery.get(QueryParams.selectedTimelineQuery) || '',
|
||||
);
|
||||
const timelineQueryData: SelectedTimelineQuery = useMemo(
|
||||
() => (timelineQuery ? JSON.parse(timelineQuery) : {}),
|
||||
[timelineQuery],
|
||||
);
|
||||
|
||||
const paginationConfig = useMemo(
|
||||
() =>
|
||||
tableData?.length > 20 && {
|
||||
pageSize: 20,
|
||||
showTotal: showPaginationItem,
|
||||
showSizeChanger: false,
|
||||
hideOnSinglePage: true,
|
||||
},
|
||||
[tableData],
|
||||
);
|
||||
|
||||
const props: ConsumerLagPayload = useMemo(
|
||||
() => ({
|
||||
start: (timelineQueryData?.start || 0) * 1e9,
|
||||
end: (timelineQueryData?.end || 0) * 1e9,
|
||||
variables: {
|
||||
partition: timelineQueryData?.partition,
|
||||
topic: timelineQueryData?.topic,
|
||||
consumer_group: timelineQueryData?.group,
|
||||
},
|
||||
detailType: currentTab,
|
||||
}),
|
||||
[currentTab, timelineQueryData],
|
||||
);
|
||||
|
||||
const handleConsumerDetailsOnError = (error: Error): void => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
|
||||
});
|
||||
};
|
||||
|
||||
const { mutate: getConsumerDetails, isLoading } = useMutation(
|
||||
getConsumerLagDetails,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
if (data.payload) {
|
||||
setColumns(getColumns(data?.payload, history));
|
||||
setTableData(getTableData(data?.payload));
|
||||
}
|
||||
},
|
||||
onError: handleConsumerDetailsOnError,
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => getConsumerDetails(props), [currentTab, props]);
|
||||
|
||||
const isEmptyDetails = (timelineQueryData: SelectedTimelineQuery): boolean =>
|
||||
isEmpty(timelineQueryData) ||
|
||||
(!timelineQueryData?.group &&
|
||||
!timelineQueryData?.topic &&
|
||||
!timelineQueryData?.partition);
|
||||
|
||||
return (
|
||||
<div className="mq-tables-container">
|
||||
{isEmptyDetails(timelineQueryData) ? (
|
||||
<div className="no-data-style">
|
||||
<Typography.Text>
|
||||
Click on a co-ordinate above to see the details
|
||||
</Typography.Text>
|
||||
<Skeleton />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mq-table-title">
|
||||
{ConsumerLagDetailTitle[currentTab]}
|
||||
<div className="mq-table-subtitle">{`${timelineQueryData?.group || ''} ${
|
||||
timelineQueryData?.topic || ''
|
||||
} ${timelineQueryData?.partition || ''}`}</div>
|
||||
</div>
|
||||
<Table
|
||||
className="mq-table"
|
||||
pagination={paginationConfig}
|
||||
size="middle"
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
bordered={false}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessagingQueuesTable;
|
||||
@@ -0,0 +1,61 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ConsumerLagDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
export interface ConsumerLagPayload {
|
||||
start?: number | string;
|
||||
end?: number | string;
|
||||
variables: {
|
||||
partition?: string;
|
||||
topic?: string;
|
||||
consumer_group?: string;
|
||||
};
|
||||
detailType: ConsumerLagDetailType;
|
||||
}
|
||||
|
||||
export interface MessagingQueuesPayloadProps {
|
||||
status: string;
|
||||
payload: {
|
||||
resultType: string;
|
||||
result: {
|
||||
table: {
|
||||
columns: {
|
||||
name: string;
|
||||
queryName: string;
|
||||
isValueColumn: boolean;
|
||||
}[];
|
||||
rows: {
|
||||
data: Record<string, string>;
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export const getConsumerLagDetails = async (
|
||||
props: ConsumerLagPayload,
|
||||
): Promise<
|
||||
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
|
||||
> => {
|
||||
const { detailType, ...restProps } = props;
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`/messaging-queues/kafka/consumer-lag/${props.detailType}`,
|
||||
{
|
||||
...restProps,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
.mq-config {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
235
frontend/src/pages/MessagingQueues/MQGraph/MQConfigOptions.tsx
Normal file
235
frontend/src/pages/MessagingQueues/MQGraph/MQConfigOptions.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import './MQConfigOptions.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Select, Spin, Tooltip } from 'antd';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { History, Location } from 'history';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Check, Share2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
import { SelectMaxTagPlaceholder } from '../MQCommon/MQCommon';
|
||||
import { useGetAllConfigOptions } from './useGetAllConfigOptions';
|
||||
|
||||
type ConfigOptionType = 'group' | 'topic' | 'partition';
|
||||
|
||||
const getPlaceholder = (type: ConfigOptionType): string => {
|
||||
switch (type) {
|
||||
case 'group':
|
||||
return 'Consumer Groups';
|
||||
case 'topic':
|
||||
return 'Topics';
|
||||
case 'partition':
|
||||
return 'Partitions';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const useConfigOptions = (
|
||||
type: ConfigOptionType,
|
||||
): {
|
||||
searchText: string;
|
||||
handleSearch: (value: string) => void;
|
||||
isFetching: boolean;
|
||||
options: DefaultOptionType[];
|
||||
} => {
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const { isFetching, options } = useGetAllConfigOptions({
|
||||
attributeKey: type,
|
||||
searchText,
|
||||
});
|
||||
const handleDebouncedSearch = useDebouncedFn((searchText): void => {
|
||||
setSearchText(searchText as string);
|
||||
}, 500);
|
||||
|
||||
const handleSearch = (value: string): void => {
|
||||
handleDebouncedSearch(value || '');
|
||||
};
|
||||
|
||||
return { searchText, handleSearch, isFetching, options };
|
||||
};
|
||||
|
||||
function setQueryParamsForConfigOptions(
|
||||
value: string[],
|
||||
urlQuery: URLSearchParams,
|
||||
history: History<unknown>,
|
||||
location: Location<unknown>,
|
||||
queryParams: QueryParams,
|
||||
): void {
|
||||
urlQuery.set(queryParams, value.join(','));
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
}
|
||||
|
||||
function getConfigValuesFromQueryParams(
|
||||
queryParams: QueryParams,
|
||||
urlQuery: URLSearchParams,
|
||||
): string[] {
|
||||
const value = urlQuery.get(queryParams);
|
||||
return value ? value.split(',') : [];
|
||||
}
|
||||
|
||||
function MessagingQueuesConfigOptions(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
const resetTabularConfigDetailsOnChange = (): void => {
|
||||
urlQuery.delete(QueryParams.selectedTimelineQuery);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
};
|
||||
|
||||
const {
|
||||
handleSearch: handleConsumerGrpSearch,
|
||||
isFetching: isFetchingConsumerGrp,
|
||||
options: consumerGrpOptions,
|
||||
} = useConfigOptions('group');
|
||||
const {
|
||||
handleSearch: handleTopicSearch,
|
||||
isFetching: isFetchingTopic,
|
||||
options: topicOptions,
|
||||
} = useConfigOptions('topic');
|
||||
const {
|
||||
handleSearch: handlePartitionSearch,
|
||||
isFetching: isFetchingPartition,
|
||||
options: partitionOptions,
|
||||
} = useConfigOptions('partition');
|
||||
|
||||
const [isURLCopied, setIsURLCopied] = useState(false);
|
||||
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<div className="mq-config">
|
||||
<div className="config-options">
|
||||
<Select
|
||||
placeholder={getPlaceholder('group')}
|
||||
showSearch
|
||||
mode="multiple"
|
||||
options={consumerGrpOptions}
|
||||
loading={isFetchingConsumerGrp}
|
||||
className="config-select-option"
|
||||
onSearch={handleConsumerGrpSearch}
|
||||
maxTagCount={4}
|
||||
maxTagPlaceholder={SelectMaxTagPlaceholder}
|
||||
value={
|
||||
getConfigValuesFromQueryParams(QueryParams.consumerGrp, urlQuery) || []
|
||||
}
|
||||
notFoundContent={
|
||||
isFetchingConsumerGrp ? (
|
||||
<span>
|
||||
<Spin size="small" /> Loading...
|
||||
</span>
|
||||
) : (
|
||||
<span>No Consumer Groups found</span>
|
||||
)
|
||||
}
|
||||
onChange={(value): void => {
|
||||
handleConsumerGrpSearch('');
|
||||
setQueryParamsForConfigOptions(
|
||||
value,
|
||||
urlQuery,
|
||||
history,
|
||||
location,
|
||||
QueryParams.consumerGrp,
|
||||
);
|
||||
resetTabularConfigDetailsOnChange();
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
placeholder={getPlaceholder('topic')}
|
||||
showSearch
|
||||
mode="multiple"
|
||||
options={topicOptions}
|
||||
loading={isFetchingTopic}
|
||||
onSearch={handleTopicSearch}
|
||||
className="config-select-option"
|
||||
maxTagCount={4}
|
||||
value={getConfigValuesFromQueryParams(QueryParams.topic, urlQuery) || []}
|
||||
maxTagPlaceholder={SelectMaxTagPlaceholder}
|
||||
notFoundContent={
|
||||
isFetchingTopic ? (
|
||||
<span>
|
||||
<Spin size="small" /> Loading...
|
||||
</span>
|
||||
) : (
|
||||
<span>No Topics found</span>
|
||||
)
|
||||
}
|
||||
onChange={(value): void => {
|
||||
handleTopicSearch('');
|
||||
setQueryParamsForConfigOptions(
|
||||
value,
|
||||
urlQuery,
|
||||
history,
|
||||
location,
|
||||
QueryParams.topic,
|
||||
);
|
||||
resetTabularConfigDetailsOnChange();
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
placeholder={getPlaceholder('partition')}
|
||||
showSearch
|
||||
mode="multiple"
|
||||
options={partitionOptions}
|
||||
loading={isFetchingPartition}
|
||||
className="config-select-option"
|
||||
onSearch={handlePartitionSearch}
|
||||
maxTagCount={4}
|
||||
value={
|
||||
getConfigValuesFromQueryParams(QueryParams.partition, urlQuery) || []
|
||||
}
|
||||
maxTagPlaceholder={SelectMaxTagPlaceholder}
|
||||
notFoundContent={
|
||||
isFetchingPartition ? (
|
||||
<span>
|
||||
<Spin size="small" /> Loading...
|
||||
</span>
|
||||
) : (
|
||||
<span>No Partitions found</span>
|
||||
)
|
||||
}
|
||||
onChange={(value): void => {
|
||||
handlePartitionSearch('');
|
||||
setQueryParamsForConfigOptions(
|
||||
value,
|
||||
urlQuery,
|
||||
history,
|
||||
location,
|
||||
QueryParams.partition,
|
||||
);
|
||||
resetTabularConfigDetailsOnChange();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip title="Share this" arrow={false}>
|
||||
<Button
|
||||
className="periscope-btn copy-url-btn"
|
||||
onClick={(): void => {
|
||||
handleCopyToClipboard(window.location.href);
|
||||
setIsURLCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsURLCopied(false);
|
||||
}, 1000);
|
||||
}}
|
||||
icon={
|
||||
isURLCopied ? (
|
||||
<Check size={14} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<Share2 size={14} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessagingQueuesConfigOptions;
|
||||
88
frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx
Normal file
88
frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
|
||||
import {
|
||||
getFiltersFromConfigOptions,
|
||||
getWidgetQuery,
|
||||
setSelectedTimelineQuery,
|
||||
} from '../MessagingQueuesUtils';
|
||||
|
||||
function MessagingQueuesGraph(): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const consumerGrp = urlQuery.get(QueryParams.consumerGrp) || '';
|
||||
const topic = urlQuery.get(QueryParams.topic) || '';
|
||||
const partition = urlQuery.get(QueryParams.partition) || '';
|
||||
|
||||
const filterItems = useMemo(
|
||||
() => getFiltersFromConfigOptions(consumerGrp, topic, partition),
|
||||
[consumerGrp, topic, partition],
|
||||
);
|
||||
|
||||
const widgetData = useMemo(
|
||||
() => getWidgetQueryBuilder(getWidgetQuery({ filterItems })),
|
||||
[filterItems],
|
||||
);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const messagingQueueCustomTooltipText = (): HTMLDivElement => {
|
||||
const customText = document.createElement('div');
|
||||
customText.textContent = 'Click on co-ordinate to view details';
|
||||
customText.style.paddingTop = '8px';
|
||||
customText.style.paddingBottom = '2px';
|
||||
customText.style.color = '#fff';
|
||||
return customText;
|
||||
};
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number) => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch, history, pathname, urlQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
isDarkMode={isDarkMode}
|
||||
$panelType={PANEL_TYPES.TIME_SERIES}
|
||||
className="mq-graph"
|
||||
>
|
||||
<GridCard
|
||||
widget={widgetData}
|
||||
headerMenuList={[...ViewMenuAction]}
|
||||
onClickHandler={(xValue, _yValue, _mouseX, _mouseY, data): void => {
|
||||
setSelectedTimelineQuery(urlQuery, xValue, location, history, data);
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
customTooltipElement={messagingQueueCustomTooltipText()}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessagingQueuesGraph;
|
||||
@@ -0,0 +1,49 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { useQuery } from 'react-query';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export interface ConfigOptions {
|
||||
attributeKey: string;
|
||||
searchText?: string;
|
||||
}
|
||||
|
||||
export interface GetAllConfigOptionsResponse {
|
||||
options: DefaultOptionType[];
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
export function useGetAllConfigOptions(
|
||||
props: ConfigOptions,
|
||||
): GetAllConfigOptionsResponse {
|
||||
const { attributeKey, searchText } = props;
|
||||
|
||||
const { data, isLoading } = useQuery(
|
||||
['attributesValues', attributeKey, searchText],
|
||||
async () => {
|
||||
const { payload } = await getAttributesValues({
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
aggregateAttribute: 'kafka_consumer_group_lag',
|
||||
attributeKey,
|
||||
searchText: searchText ?? '',
|
||||
filterAttributeKeyDataType: DataTypes.String,
|
||||
tagType: 'tag',
|
||||
});
|
||||
|
||||
if (payload) {
|
||||
const values = Object.values(payload).find((el) => !!el) || [];
|
||||
const options: DefaultOptionType[] = values.map((val: string) => ({
|
||||
label: val,
|
||||
value: val,
|
||||
}));
|
||||
return options;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
return { options: data ?? [], isFetching: isLoading };
|
||||
}
|
||||
424
frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss
Normal file
424
frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss
Normal file
@@ -0,0 +1,424 @@
|
||||
.messaging-queue-container {
|
||||
.messaging-breadcrumb {
|
||||
display: flex;
|
||||
padding: 0px 16px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 8px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
.message-queue-text {
|
||||
cursor: pointer;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.messaging-header {
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
padding: 10px 16px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
|
||||
.header-config {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
.messaging-queue-options {
|
||||
.ant-select-selector {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: 6px 6px 6px 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messaging-queue-main-graph {
|
||||
display: flex;
|
||||
padding: 24px 16px;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.config-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.config-select-option {
|
||||
.ant-select-selector {
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
min-width: 164px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mq-graph {
|
||||
height: 420px;
|
||||
padding: 24px 24px 0 24px;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.messaging-queue-details {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
.mq-details-options {
|
||||
letter-spacing: -0.06px;
|
||||
.ant-radio-button-wrapper {
|
||||
border-color: var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
.ant-radio-button-wrapper-checked {
|
||||
background: var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
.ant-radio-button-wrapper-disabled {
|
||||
background: var(--bg-ink-400);
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
.ant-radio-button-wrapper::before {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.disabled-option {
|
||||
.coming-soon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messaging-queue-options-popup {
|
||||
width: 264px !important;
|
||||
}
|
||||
|
||||
.messaging-overview {
|
||||
padding: 24px 16px 10px 16px;
|
||||
|
||||
.overview-text {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: Inter;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.08px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.overview-subtext {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
margin: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.overview-doc-area {
|
||||
margin: 16px 0 28px 0;
|
||||
display: flex;
|
||||
|
||||
.middle-card {
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.overview-info-card {
|
||||
display: flex;
|
||||
width: 376px;
|
||||
min-height: 176px;
|
||||
padding: 18px 20px 20px 20px;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 2px;
|
||||
|
||||
.card-title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.52px;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-info-text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
margin: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.button-grp {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.ant-btn {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.ant-btn-default {
|
||||
background-color: var(--bg-slate-400);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
display: flex;
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
width: 337px;
|
||||
height: 283px;
|
||||
border-radius: 2px;
|
||||
|
||||
.summary-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
|
||||
> p {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500; /* 169.231% */
|
||||
letter-spacing: 0.52px;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
> p {
|
||||
color: var(--bg-slate-200);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.48px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-detail-btn {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coming-soon-card {
|
||||
background: var(--bg-ink-500) !important;
|
||||
border-left: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-confirm-modal {
|
||||
background-color: var(--bg-ink-500);
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
.ant-modal-content {
|
||||
background-color: var(--bg-ink-300);
|
||||
.ant-modal-confirm-content {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-modal-confirm-body-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 150px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.messaging-queue-container {
|
||||
.messaging-breadcrumb {
|
||||
color: var(--bg-ink-400);
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
.messaging-header {
|
||||
color: var(--bg-ink-400);
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.header-config {
|
||||
.messaging-queue-options {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messaging-queue-main-graph {
|
||||
.config-options {
|
||||
.config-select-option {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.messaging-queue-details {
|
||||
.mq-details-options {
|
||||
.ant-radio-button-wrapper {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
.ant-radio-button-wrapper-checked {
|
||||
color: var(--bg-slate-200);
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
.ant-radio-button-wrapper-disabled {
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messaging-overview {
|
||||
.overview-text {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
|
||||
.overview-subtext {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.overview-doc-area {
|
||||
.overview-info-card {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.card-title {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
|
||||
.card-info-text {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.button-grp {
|
||||
.ant-btn-default {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
.summary-card {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.summary-title {
|
||||
> p {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
|
||||
.time-value {
|
||||
> p {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coming-soon-card {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-confirm-modal {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
.ant-modal-content {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
.ant-modal-confirm-content {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
174
frontend/src/pages/MessagingQueues/MessagingQueues.tsx
Normal file
174
frontend/src/pages/MessagingQueues/MessagingQueues.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import './MessagingQueues.styles.scss';
|
||||
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Modal } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { Calendar, ListMinus } from 'lucide-react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
MessagingQueuesViewType,
|
||||
} from './MessagingQueuesUtils';
|
||||
import { ComingSoon } from './MQCommon/MQCommon';
|
||||
|
||||
function MessagingQueues(): JSX.Element {
|
||||
const history = useHistory();
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
const showConfirm = (): void => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleFilled />,
|
||||
content:
|
||||
'Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.',
|
||||
className: 'overview-confirm-modal',
|
||||
onOk() {
|
||||
history.push(ROUTES.MESSAGING_QUEUES_DETAIL);
|
||||
},
|
||||
okText: 'Proceed',
|
||||
});
|
||||
};
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
|
||||
const getStartedRedirect = (link: string): void => {
|
||||
if (isCloudUserVal) {
|
||||
history.push(link);
|
||||
} else {
|
||||
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="messaging-queue-container">
|
||||
<div className="messaging-breadcrumb">
|
||||
<ListMinus size={16} />
|
||||
Messaging Queues
|
||||
</div>
|
||||
<div className="messaging-header">
|
||||
<div className="header-config">Kafka / Overview</div>
|
||||
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
|
||||
</div>
|
||||
<div className="messaging-overview">
|
||||
<p className="overview-text">
|
||||
Start sending data in as little as 20 minutes
|
||||
</p>
|
||||
<p className="overview-subtext">Connect and Monitor Your Data Streams</p>
|
||||
<div className="overview-doc-area">
|
||||
<div className="overview-info-card">
|
||||
<div>
|
||||
<p className="card-title">Configure Consumer</p>
|
||||
<p className="card-info-text">
|
||||
Connect your consumer and producer data sources to start monitoring.
|
||||
</p>
|
||||
</div>
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
getStartedRedirect(ROUTES.GET_STARTED_APPLICATION_MONITORING)
|
||||
}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-info-card middle-card">
|
||||
<div>
|
||||
<p className="card-title">Configure Producer</p>
|
||||
<p className="card-info-text">
|
||||
Connect your consumer and producer data sources to start monitoring.
|
||||
</p>
|
||||
</div>
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
getStartedRedirect(ROUTES.GET_STARTED_APPLICATION_MONITORING)
|
||||
}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-info-card">
|
||||
<div>
|
||||
<p className="card-title">Monitor kafka</p>
|
||||
<p className="card-info-text">
|
||||
Set up your Kafka monitoring to track consumer and producer activities.
|
||||
</p>
|
||||
</div>
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
getStartedRedirect(ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING)
|
||||
}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-section">
|
||||
<div className="summary-card">
|
||||
<div className="summary-title">
|
||||
<p>{MessagingQueuesViewType.consumerLag.label}</p>
|
||||
<div className="time-value">
|
||||
<Calendar size={14} color={Color.BG_SLATE_200} />
|
||||
<p className="time-value">1D</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="view-detail-btn">
|
||||
<Button type="primary" onClick={showConfirm}>
|
||||
View Details
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-card coming-soon-card">
|
||||
<div className="summary-title">
|
||||
<p>{MessagingQueuesViewType.partitionLatency.label}</p>
|
||||
<div className="time-value">
|
||||
<Calendar size={14} color={Color.BG_SLATE_200} />
|
||||
<p className="time-value">1D</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="view-detail-btn">
|
||||
<ComingSoon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-card coming-soon-card">
|
||||
<div className="summary-title">
|
||||
<p>{MessagingQueuesViewType.producerLatency.label}</p>
|
||||
<div className="time-value">
|
||||
<Calendar size={14} color={Color.BG_SLATE_200} />
|
||||
<p className="time-value">1D</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="view-detail-btn">
|
||||
<ComingSoon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-card coming-soon-card">
|
||||
<div className="summary-title">
|
||||
<p>{MessagingQueuesViewType.consumerLatency.label}</p>
|
||||
<div className="time-value">
|
||||
<Calendar size={14} color={Color.BG_SLATE_200} />
|
||||
<p className="time-value">1D</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="view-detail-btn">
|
||||
<ComingSoon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessagingQueues;
|
||||
225
frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts
Normal file
225
frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
|
||||
import { History, Location } from 'history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export const KAFKA_SETUP_DOC_LINK =
|
||||
'https://github.com/shivanshuraj1333/kafka-opentelemetry-instrumentation/tree/master';
|
||||
|
||||
export function convertToTitleCase(text: string): string {
|
||||
return text
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export type RowData = {
|
||||
key: string | number;
|
||||
[key: string]: string | number;
|
||||
};
|
||||
|
||||
export enum ConsumerLagDetailType {
|
||||
ConsumerDetails = 'consumer-details',
|
||||
ProducerDetails = 'producer-details',
|
||||
NetworkLatency = 'network-latency',
|
||||
PartitionHostMetrics = 'partition-host-metric',
|
||||
}
|
||||
|
||||
export const ConsumerLagDetailTitle: Record<ConsumerLagDetailType, string> = {
|
||||
'consumer-details': 'Consumer Groups Details',
|
||||
'producer-details': 'Producer Details',
|
||||
'network-latency': 'Network Latency',
|
||||
'partition-host-metric': 'Partition Host Metrics',
|
||||
};
|
||||
|
||||
export function createWidgetFilterItem(
|
||||
key: string,
|
||||
value: string,
|
||||
): TagFilterItem {
|
||||
const id = `${key}--string--tag--false`;
|
||||
|
||||
return {
|
||||
id: uuid(),
|
||||
key: {
|
||||
key,
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
id,
|
||||
},
|
||||
op: '=',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFiltersFromConfigOptions(
|
||||
consumerGrp?: string,
|
||||
topic?: string,
|
||||
partition?: string,
|
||||
): TagFilterItem[] {
|
||||
const configOptions = [
|
||||
{ key: 'group', values: consumerGrp?.split(',') },
|
||||
{ key: 'topic', values: topic?.split(',') },
|
||||
{ key: 'partition', values: partition?.split(',') },
|
||||
];
|
||||
return configOptions.reduce<TagFilterItem[]>(
|
||||
(accumulator, { key, values }) => {
|
||||
if (values && !isEmpty(values.filter((item) => item !== ''))) {
|
||||
accumulator.push(
|
||||
...values.map((value) => createWidgetFilterItem(key, value)),
|
||||
);
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
export function getWidgetQuery({
|
||||
filterItems,
|
||||
}: {
|
||||
filterItems: TagFilterItem[];
|
||||
}): GetWidgetQueryBuilderProps {
|
||||
return {
|
||||
title: 'Consumer Lag',
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
fillSpans: false,
|
||||
yAxisUnit: 'none',
|
||||
query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
promql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'kafka_consumer_group_lag--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'kafka_consumer_group_lag',
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'max',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: filterItems || [],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'group--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'group',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'topic--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'topic',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'partition--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'partition',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: '{{group}}-{{topic}}-{{partition}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'avg',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'max',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const convertToNanoseconds = (timestamp: number): bigint =>
|
||||
BigInt((timestamp * 1e9).toFixed(0));
|
||||
|
||||
export const getStartAndEndTimesInMilliseconds = (
|
||||
timestamp: number,
|
||||
): { start: number; end: number } => {
|
||||
const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; // 5 minutes in milliseconds - check with Shivanshu once
|
||||
|
||||
const start = Math.floor(timestamp);
|
||||
const end = Math.floor(start + FIVE_MINUTES_IN_MILLISECONDS);
|
||||
|
||||
return { start, end };
|
||||
};
|
||||
|
||||
export interface SelectedTimelineQuery {
|
||||
group?: string;
|
||||
partition?: string;
|
||||
topic?: string;
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
export function setSelectedTimelineQuery(
|
||||
urlQuery: URLSearchParams,
|
||||
timestamp: number,
|
||||
location: Location<unknown>,
|
||||
history: History<unknown>,
|
||||
data?: {
|
||||
[key: string]: string;
|
||||
},
|
||||
): void {
|
||||
const selectedTimelineQuery: SelectedTimelineQuery = {
|
||||
group: data?.group,
|
||||
partition: data?.partition,
|
||||
topic: data?.topic,
|
||||
...getStartAndEndTimesInMilliseconds(timestamp),
|
||||
};
|
||||
urlQuery.set(
|
||||
QueryParams.selectedTimelineQuery,
|
||||
encodeURIComponent(JSON.stringify(selectedTimelineQuery)),
|
||||
);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
}
|
||||
|
||||
export const MessagingQueuesViewType = {
|
||||
consumerLag: {
|
||||
label: 'Consumer Lag view',
|
||||
value: 'consumerLag',
|
||||
},
|
||||
partitionLatency: {
|
||||
label: 'Partition Latency view',
|
||||
value: 'partitionLatency',
|
||||
},
|
||||
producerLatency: {
|
||||
label: 'Producer Latency view',
|
||||
value: 'producerLatency',
|
||||
},
|
||||
consumerLatency: {
|
||||
label: 'Consumer latency view',
|
||||
value: 'consumerLatency',
|
||||
},
|
||||
};
|
||||
3
frontend/src/pages/MessagingQueues/index.tsx
Normal file
3
frontend/src/pages/MessagingQueues/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import MessagingQueues from './MessagingQueues';
|
||||
|
||||
export default MessagingQueues;
|
||||
172
frontend/src/pages/SaveView/__test__/SaveView.test.tsx
Normal file
172
frontend/src/pages/SaveView/__test__/SaveView.test.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import ROUTES from 'constants/routes';
|
||||
import { explorerView } from 'mocks-server/__mockdata__/explorer_views';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils';
|
||||
|
||||
import SaveView from '..';
|
||||
|
||||
const handleExplorerTabChangeTest = jest.fn();
|
||||
jest.mock('hooks/useHandleExplorerTabChange', () => ({
|
||||
useHandleExplorerTabChange: () => ({
|
||||
handleExplorerTabChange: handleExplorerTabChangeTest,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn().mockReturnValue({
|
||||
pathname: `${ROUTES.TRACES_SAVE_VIEWS}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('SaveView', () => {
|
||||
it('should render the SaveView component', async () => {
|
||||
render(<SaveView />);
|
||||
expect(await screen.findByText('Table View')).toBeInTheDocument();
|
||||
|
||||
const savedViews = screen.getAllByRole('row');
|
||||
expect(savedViews).toHaveLength(2);
|
||||
|
||||
// assert row 1
|
||||
expect(
|
||||
within(document.querySelector('.view-tag') as HTMLElement).getByText('T'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('test-user-1')).toBeInTheDocument();
|
||||
|
||||
// assert row 2
|
||||
expect(screen.getByText('R-test panel')).toBeInTheDocument();
|
||||
expect(screen.getByText('test-user-test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('explorer icon should take the user to the related explorer page', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={[ROUTES.TRACES_SAVE_VIEWS]}>
|
||||
<Route path={ROUTES.TRACES_SAVE_VIEWS}>
|
||||
<SaveView />
|
||||
</Route>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Table View')).toBeInTheDocument();
|
||||
|
||||
const explorerIcon = await screen.findAllByTestId('go-to-explorer');
|
||||
expect(explorerIcon[0]).toBeInTheDocument();
|
||||
|
||||
// Simulate click on explorer icon
|
||||
fireEvent.click(explorerIcon[0]);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(handleExplorerTabChangeTest).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
'/traces-explorer', // Asserts the third argument is '/traces-explorer'
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the SaveView component with a search input', async () => {
|
||||
render(<SaveView />);
|
||||
const searchInput = screen.getByPlaceholderText('Search for views...');
|
||||
expect(await screen.findByText('Table View')).toBeInTheDocument();
|
||||
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
|
||||
// search for 'R-test panel'
|
||||
searchInput.focus();
|
||||
(searchInput as HTMLInputElement).setSelectionRange(
|
||||
0,
|
||||
(searchInput as HTMLInputElement).value.length,
|
||||
);
|
||||
|
||||
fireEvent.change(searchInput, { target: { value: 'R-test panel' } });
|
||||
expect(searchInput).toHaveValue('R-test panel');
|
||||
searchInput.blur();
|
||||
|
||||
expect(await screen.findByText('R-test panel')).toBeInTheDocument();
|
||||
|
||||
// Table View should not be present now
|
||||
const savedViews = screen.getAllByRole('row');
|
||||
expect(savedViews).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should be able to edit name of view', async () => {
|
||||
server.use(
|
||||
rest.put(
|
||||
'http://localhost/api/v1/explorer/views/test-uuid-1',
|
||||
// eslint-disable-next-line no-return-assign
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...explorerView,
|
||||
data: [
|
||||
...explorerView.data,
|
||||
(explorerView.data[0].name = 'New Table View'),
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
render(<SaveView />);
|
||||
|
||||
const editButton = await screen.findAllByTestId('edit-view');
|
||||
fireEvent.click(editButton[0]);
|
||||
|
||||
const viewName = await screen.findByTestId('view-name');
|
||||
expect(viewName).toBeInTheDocument();
|
||||
expect(viewName).toHaveValue('Table View');
|
||||
|
||||
const newViewName = 'New Table View';
|
||||
fireEvent.change(viewName, { target: { value: newViewName } });
|
||||
expect(viewName).toHaveValue(newViewName);
|
||||
|
||||
const saveButton = await screen.findByTestId('save-view');
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(newViewName)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to delete a view', async () => {
|
||||
server.use(
|
||||
rest.delete(
|
||||
'http://localhost/api/v1/explorer/views/test-uuid-1',
|
||||
(_req, res, ctx) => res(ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<SaveView />);
|
||||
|
||||
const deleteButton = await screen.findAllByTestId('delete-view');
|
||||
fireEvent.click(deleteButton[0]);
|
||||
|
||||
expect(await screen.findByText('delete_confirm_message')).toBeInTheDocument();
|
||||
|
||||
const confirmButton = await screen.findByTestId('confirm-delete');
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('Table View')).toBeNull());
|
||||
});
|
||||
|
||||
it('should render empty state', async () => {
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/explorer/views', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: [],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
render(<SaveView />);
|
||||
|
||||
expect(screen.getByText('No data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -263,13 +263,19 @@ function SaveView(): JSX.Element {
|
||||
<PenLine
|
||||
size={14}
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
data-testid="edit-view"
|
||||
onClick={(): void => handleEditModelOpen(view, bgColor)}
|
||||
/>
|
||||
<Compass size={14} onClick={(): void => handleRedirectQuery(view)} />
|
||||
<Compass
|
||||
size={14}
|
||||
onClick={(): void => handleRedirectQuery(view)}
|
||||
data-testid="go-to-explorer"
|
||||
/>
|
||||
<Trash2
|
||||
size={14}
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
color={Color.BG_CHERRY_500}
|
||||
data-testid="delete-view"
|
||||
onClick={(): void => handleDeleteModelOpen(view.uuid, view.name)}
|
||||
/>
|
||||
</div>
|
||||
@@ -347,6 +353,7 @@ function SaveView(): JSX.Element {
|
||||
onClick={onDeleteHandler}
|
||||
className="delete-btn"
|
||||
disabled={isDeleteLoading}
|
||||
data-testid="confirm-delete"
|
||||
>
|
||||
Delete view
|
||||
</Button>,
|
||||
@@ -371,6 +378,7 @@ function SaveView(): JSX.Element {
|
||||
icon={<Check size={16} color={Color.BG_VANILLA_100} />}
|
||||
onClick={onUpdateQueryHandler}
|
||||
disabled={isViewUpdating}
|
||||
data-testid="save-view"
|
||||
>
|
||||
Save changes
|
||||
</Button>,
|
||||
@@ -385,6 +393,7 @@ function SaveView(): JSX.Element {
|
||||
<Input
|
||||
placeholder="e.g. Crash landing view"
|
||||
value={newViewName}
|
||||
data-testid="view-name"
|
||||
onChange={(e): void => setNewViewName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
214
frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx
Normal file
214
frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
|
||||
import TraceDetail from '..';
|
||||
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string; search: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACE_DETAIL}`,
|
||||
search: '?spanId=28a8a67365d0bd8b&levelUp=0&levelDown=0',
|
||||
}),
|
||||
|
||||
useParams: jest.fn().mockReturnValue({
|
||||
id: '000000000000000071dc9b0a338729b4',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/TraceFlameGraph/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div>TraceFlameGraph</div>,
|
||||
}));
|
||||
|
||||
describe('TraceDetail', () => {
|
||||
it('should render tracedetail', async () => {
|
||||
const { findByText, getByText, getAllByText, getByPlaceholderText } = render(
|
||||
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
|
||||
<Route path={ROUTES.TRACE_DETAIL}>
|
||||
<TraceDetail />
|
||||
</Route>
|
||||
,
|
||||
</MemoryRouter>,
|
||||
);
|
||||
expect(await findByText('Trace Details')).toBeInTheDocument();
|
||||
|
||||
// as we have an active spanId, it should scroll to the selected span
|
||||
expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled();
|
||||
|
||||
// assertions
|
||||
expect(getByText('TraceFlameGraph')).toBeInTheDocument();
|
||||
expect(getByText('Focus on selected span')).toBeInTheDocument();
|
||||
|
||||
// span action buttons
|
||||
expect(getByText('Reset Focus')).toBeInTheDocument();
|
||||
expect(getByText('50 Spans')).toBeInTheDocument();
|
||||
|
||||
// trace span detail - parent -> child
|
||||
expect(getAllByText('frontend')[0]).toBeInTheDocument();
|
||||
expect(getByText('776.76 ms')).toBeInTheDocument();
|
||||
[
|
||||
{ trace: 'HTTP GET /dispatch', duration: '776.76 ms', count: '50' },
|
||||
{ trace: 'HTTP GET: /customer', duration: '349.44 ms', count: '4' },
|
||||
{
|
||||
trace: '/driver.DriverService/FindNearest',
|
||||
duration: '173.10 ms',
|
||||
count: '15',
|
||||
},
|
||||
// and so on ...
|
||||
].forEach((traceDetail) => {
|
||||
expect(getByText(traceDetail.trace)).toBeInTheDocument();
|
||||
expect(getByText(traceDetail.duration)).toBeInTheDocument();
|
||||
expect(getByText(traceDetail.count)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Details for selected Span
|
||||
expect(getByText('Details for selected Span')).toBeInTheDocument();
|
||||
['Service', 'Operation', 'SpanKind', 'StatusCodeString'].forEach((detail) => {
|
||||
expect(getByText(detail)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// go to related logs button
|
||||
const goToRelatedLogsButton = getByText('Go to Related logs');
|
||||
expect(goToRelatedLogsButton).toBeInTheDocument();
|
||||
|
||||
// Tag and Event tabs
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Events')).toBeInTheDocument();
|
||||
expect(getByPlaceholderText('traceDetails:search_tags')).toBeInTheDocument();
|
||||
|
||||
// Tag details
|
||||
[
|
||||
{ title: 'client-uuid', value: '64a18ffd5f8adbfb' },
|
||||
{ title: 'component', value: 'net/http' },
|
||||
{ title: 'host.name', value: '4f6ec470feea' },
|
||||
{ title: 'http.method', value: 'GET' },
|
||||
{ title: 'http.url', value: '/route?dropoff=728%2C326&pickup=165%2C543' },
|
||||
{ title: 'http.status_code', value: '200' },
|
||||
{ title: 'ip', value: '172.25.0.2' },
|
||||
{ title: 'opencensus.exporterversion', value: 'Jaeger-Go-2.30.0' },
|
||||
].forEach((tag) => {
|
||||
expect(getByText(tag.title)).toBeInTheDocument();
|
||||
expect(getByText(tag.value)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// see full value
|
||||
expect(getAllByText('View full value')[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render tracedetail events tab', async () => {
|
||||
const { findByText, getByText } = render(
|
||||
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
|
||||
<Route path={ROUTES.TRACE_DETAIL}>
|
||||
<TraceDetail />
|
||||
</Route>
|
||||
,
|
||||
</MemoryRouter>,
|
||||
);
|
||||
expect(await findByText('Trace Details')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(getByText('Events'));
|
||||
|
||||
expect(await screen.findByText('HTTP request received')).toBeInTheDocument();
|
||||
|
||||
// event details
|
||||
[
|
||||
{ title: 'Event Start Time', value: '527.60 ms' },
|
||||
{ title: 'level', value: 'info' },
|
||||
].forEach((tag) => {
|
||||
expect(getByText(tag.title)).toBeInTheDocument();
|
||||
expect(getByText(tag.value)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(getByText('View full log event message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle slider - selected span details', async () => {
|
||||
const { findByTestId, queryByText } = render(
|
||||
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
|
||||
<Route path={ROUTES.TRACE_DETAIL}>
|
||||
<TraceDetail />
|
||||
</Route>
|
||||
,
|
||||
</MemoryRouter>,
|
||||
);
|
||||
const slider = await findByTestId('span-details-sider');
|
||||
expect(slider).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(
|
||||
slider.querySelector('.ant-layout-sider-trigger') as HTMLElement,
|
||||
);
|
||||
|
||||
expect(queryByText('Details for selected Span')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to selected another span and see its detail', async () => {
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
|
||||
<Route path={ROUTES.TRACE_DETAIL}>
|
||||
<TraceDetail />
|
||||
</Route>
|
||||
,
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Trace Details')).toBeInTheDocument();
|
||||
|
||||
const spanTitle = getByText('/driver.DriverService/FindNearest');
|
||||
expect(spanTitle).toBeInTheDocument();
|
||||
fireEvent.click(spanTitle);
|
||||
|
||||
// Tag details
|
||||
[
|
||||
{ title: 'client-uuid', value: '6fb81b8ca91b2b4d' },
|
||||
{ title: 'component', value: 'gRPC' },
|
||||
{ title: 'host.name', value: '4f6ec470feea' },
|
||||
].forEach((tag) => {
|
||||
expect(getByText(tag.title)).toBeInTheDocument();
|
||||
expect(getByText(tag.value)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('focus on selected span and reset focus action', async () => {
|
||||
const { getByText, getAllByText } = render(
|
||||
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>
|
||||
<Route path={ROUTES.TRACE_DETAIL}>
|
||||
<TraceDetail />
|
||||
</Route>
|
||||
,
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Trace Details')).toBeInTheDocument();
|
||||
|
||||
const spanTitle = getByText('/driver.DriverService/FindNearest');
|
||||
expect(spanTitle).toBeInTheDocument();
|
||||
fireEvent.click(spanTitle);
|
||||
|
||||
expect(await screen.findByText('6fb81b8ca91b2b4d')).toBeInTheDocument();
|
||||
|
||||
// focus on selected span
|
||||
const focusButton = getByText('Focus on selected span');
|
||||
expect(focusButton).toBeInTheDocument();
|
||||
fireEvent.click(focusButton);
|
||||
|
||||
// assert selected span
|
||||
expect(getByText('15 Spans')).toBeInTheDocument();
|
||||
expect(getAllByText('/driver.DriverService/FindNearest')).toHaveLength(3);
|
||||
expect(getByText('173.10 ms')).toBeInTheDocument();
|
||||
|
||||
// reset focus
|
||||
expect(screen.queryByText('HTTP GET /dispatch')).not.toBeInTheDocument();
|
||||
|
||||
const resetFocusButton = getByText('Reset Focus');
|
||||
expect(resetFocusButton).toBeInTheDocument();
|
||||
fireEvent.click(resetFocusButton);
|
||||
|
||||
expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled();
|
||||
expect(screen.queryByText('HTTP GET /dispatch')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -617,9 +617,7 @@ describe('TracesExplorer - ', () => {
|
||||
const viewListOptions = await screen.findByRole('listbox');
|
||||
expect(viewListOptions).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
within(viewListOptions).getByText('success traces list view'),
|
||||
).toBeInTheDocument();
|
||||
expect(within(viewListOptions).getByText('R-test panel')).toBeInTheDocument();
|
||||
|
||||
expect(within(viewListOptions).getByText('Table View')).toBeInTheDocument();
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
ALL_CHANNELS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
INGESTION_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ALL_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
MESSAGING_QUEUES: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
MESSAGING_QUEUES_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ALL_ERROR: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
APPLICATION: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
CHANNELS_EDIT: ['ADMIN'],
|
||||
@@ -95,7 +97,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
API_KEYS: ['ADMIN'],
|
||||
LOGS_BASE: [],
|
||||
OLD_LOGS_EXPLORER: [],
|
||||
OLD_LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
||||
2
go.mod
2
go.mod
@@ -6,7 +6,7 @@ require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.23.2
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.2
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.7
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
|
||||
4
go.sum
4
go.sum
@@ -64,8 +64,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
||||
github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg=
|
||||
github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.2 h1:SmjsBZjMjTVVpuOlfJXlsDJQbdefQP/9Wz3CyzSuZuU=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.2/go.mod h1:ISAXYhZenojCWg6CdDJtPMpfS6Zwc08+uoxH25tc6Y0=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.7 h1:UBjO88GNCGZuWKl1LFukRahR1cu9AGwFHyObo07RrYA=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.7/go.mod h1:3s9cSL8yexkBBMfK9mC3WWrAPm8oMtlZhvBxvt+Ziag=
|
||||
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
|
||||
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=
|
||||
|
||||
@@ -805,7 +805,7 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request
|
||||
continue
|
||||
}
|
||||
filterItems := []v3.FilterItem{}
|
||||
if rule.AlertType == "LOGS_BASED_ALERT" || rule.AlertType == "TRACES_BASED_ALERT" {
|
||||
if rule.AlertType == rules.AlertTypeLogs || rule.AlertType == rules.AlertTypeTraces {
|
||||
if rule.RuleCondition.CompositeQuery != nil {
|
||||
if rule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
|
||||
for _, query := range rule.RuleCondition.CompositeQuery.BuilderQueries {
|
||||
@@ -818,9 +818,9 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
newFilters := common.PrepareFilters(lbls, filterItems)
|
||||
ts := time.Unix(res.Items[idx].UnixMilli/1000, 0)
|
||||
if rule.AlertType == "LOGS_BASED_ALERT" {
|
||||
if rule.AlertType == rules.AlertTypeLogs {
|
||||
res.Items[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, newFilters)
|
||||
} else if rule.AlertType == "TRACES_BASED_ALERT" {
|
||||
} else if rule.AlertType == rules.AlertTypeTraces {
|
||||
res.Items[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, newFilters)
|
||||
}
|
||||
}
|
||||
@@ -854,9 +854,9 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter,
|
||||
}
|
||||
ts := time.Unix(params.End/1000, 0)
|
||||
filters := common.PrepareFilters(lbls, nil)
|
||||
if rule.AlertType == "LOGS_BASED_ALERT" {
|
||||
if rule.AlertType == rules.AlertTypeLogs {
|
||||
res[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, filters)
|
||||
} else if rule.AlertType == "TRACES_BASED_ALERT" {
|
||||
} else if rule.AlertType == rules.AlertTypeTraces {
|
||||
res[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, filters)
|
||||
}
|
||||
}
|
||||
@@ -2496,10 +2496,113 @@ func (aH *APIHandler) RegisterMessagingQueuesRoutes(router *mux.Router, am *Auth
|
||||
|
||||
kafkaSubRouter.HandleFunc("/producer-details", am.ViewAccess(aH.getProducerData)).Methods(http.MethodPost)
|
||||
kafkaSubRouter.HandleFunc("/consumer-details", am.ViewAccess(aH.getConsumerData)).Methods(http.MethodPost)
|
||||
kafkaSubRouter.HandleFunc("/network-latency", am.ViewAccess(aH.getNetworkData)).Methods(http.MethodPost)
|
||||
|
||||
// for other messaging queues, add SubRouters here
|
||||
}
|
||||
|
||||
// not using md5 hashing as the plain string would work
|
||||
func uniqueIdentifier(clientID, serviceInstanceID, serviceName, separator string) string {
|
||||
return clientID + separator + serviceInstanceID + separator + serviceName
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getNetworkData(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
attributeCache := &mq.Clients{
|
||||
Hash: make(map[string]struct{}),
|
||||
}
|
||||
messagingQueue, apiErr := ParseMessagingQueueBody(r)
|
||||
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := mq.BuildQRParamsNetwork(messagingQueue, "throughput", attributeCache)
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQueriesByName map[string]error
|
||||
|
||||
result, errQueriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams, nil)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQueriesByName)
|
||||
return
|
||||
}
|
||||
|
||||
for _, res := range result {
|
||||
for _, series := range res.Series {
|
||||
clientID, clientIDOk := series.Labels["client_id"]
|
||||
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
|
||||
serviceName, serviceNameOk := series.Labels["service_name"]
|
||||
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
|
||||
_, ok := attributeCache.Hash[hashKey]
|
||||
if clientIDOk && serviceInstanceIDOk && serviceNameOk && !ok {
|
||||
attributeCache.Hash[hashKey] = struct{}{}
|
||||
attributeCache.ClientID = append(attributeCache.ClientID, clientID)
|
||||
attributeCache.ServiceInstanceID = append(attributeCache.ServiceInstanceID, serviceInstanceID)
|
||||
attributeCache.ServiceName = append(attributeCache.ServiceName, serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryRangeParams, err = mq.BuildQRParamsNetwork(messagingQueue, "fetch-latency", attributeCache)
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
resultFetchLatency, errQueriesByNameFetchLatency, err := aH.querierV2.QueryRange(r.Context(), queryRangeParams, nil)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQueriesByNameFetchLatency)
|
||||
return
|
||||
}
|
||||
|
||||
latencyColumn := &v3.Result{QueryName: "latency"}
|
||||
var latencySeries []*v3.Series
|
||||
for _, res := range resultFetchLatency {
|
||||
for _, series := range res.Series {
|
||||
clientID, clientIDOk := series.Labels["client_id"]
|
||||
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
|
||||
serviceName, serviceNameOk := series.Labels["service_name"]
|
||||
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
|
||||
_, ok := attributeCache.Hash[hashKey]
|
||||
if clientIDOk && serviceInstanceIDOk && serviceNameOk && ok {
|
||||
latencySeries = append(latencySeries, series)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
latencyColumn.Series = latencySeries
|
||||
result = append(result, latencyColumn)
|
||||
|
||||
resultFetchLatency = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: resultFetchLatency,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getProducerData(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
## Consumer Lag feature break down
|
||||
|
||||
### 1) Consumer Lag Graph
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 2) Consumer Group Details
|
||||
### 1) Consumer Group Details
|
||||
|
||||
API endpoint:
|
||||
|
||||
@@ -13,75 +8,75 @@ API endpoint:
|
||||
POST /api/v1/messaging-queues/kafka/consumer-lag/consumer-details
|
||||
```
|
||||
|
||||
Request-Body
|
||||
```json
|
||||
{
|
||||
"start": 1720685296000000000,
|
||||
"end": 1721290096000000000,
|
||||
"variables": {
|
||||
"partition": "0",
|
||||
"topic": "topic1",
|
||||
"consumer_group": "cg1"
|
||||
}
|
||||
"start": 1724429217000000000,
|
||||
"end": 1724431017000000000,
|
||||
"variables": {
|
||||
"partition": "0",
|
||||
"topic": "topic1",
|
||||
"consumer_group": "cg1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
response in query range format `series`
|
||||
Response in query range `table` format
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "error_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "throughput",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "avg_msg_size",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"avg_msg_size": "0",
|
||||
"error_rate": "0",
|
||||
"p99": "0.2942205100000016",
|
||||
"service_name": "consumer-svc",
|
||||
"throughput": "0.00016534391534391533"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"name": "service_name",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "error_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "throughput",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "avg_msg_size",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"avg_msg_size": "15",
|
||||
"error_rate": "0",
|
||||
"p99": "0.47993265000000035",
|
||||
"service_name": "consumer-svc",
|
||||
"throughput": "39.86888888888889"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
|
||||
### 3) Producer Details
|
||||
### 2) Producer Details
|
||||
|
||||
API endpoint:
|
||||
|
||||
@@ -89,18 +84,19 @@ API endpoint:
|
||||
POST /api/v1/messaging-queues/kafka/consumer-lag/producer-details
|
||||
```
|
||||
|
||||
Request-Body
|
||||
```json
|
||||
{
|
||||
"start": 1720685296000000000,
|
||||
"end": 1721290096000000000,
|
||||
"start": 1724429217000000000,
|
||||
"end": 1724431017000000000,
|
||||
"variables": {
|
||||
"partition": "0",
|
||||
"partition": "0",
|
||||
"topic": "topic1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
response in query range format `series`
|
||||
Response in query range `table` format
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
@@ -116,17 +112,17 @@ response in query range format `series`
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99_query.p99",
|
||||
"name": "p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "error_rate",
|
||||
"name": "error_percentage",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "rps",
|
||||
"name": "throughput",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
}
|
||||
@@ -134,56 +130,9 @@ response in query range format `series`
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99_query.p99": "150.08830908000002",
|
||||
"rps": "0.00016534391534391533",
|
||||
"service_name": "producer-svc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
response in query range format `table`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99_query.p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "error_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "rps",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99_query.p99": "150.08830908000002",
|
||||
"rps": "0.00016534391534391533",
|
||||
"error_percentage": "0",
|
||||
"p99": "5.51359028",
|
||||
"throughput": "39.86888888888889",
|
||||
"service_name": "producer-svc"
|
||||
}
|
||||
}
|
||||
@@ -195,3 +144,85 @@ response in query range format `table`
|
||||
}
|
||||
```
|
||||
|
||||
### 3) Network Fetch Latency:
|
||||
|
||||
API endpoint:
|
||||
|
||||
```
|
||||
POST /api/v1/messaging-queues/kafka/consumer-lag/network-latency
|
||||
```
|
||||
|
||||
Request-Body
|
||||
```json
|
||||
{
|
||||
"start": 1724673937000000000,
|
||||
"end": 1724675737000000000,
|
||||
"variables": {
|
||||
"consumer_group": "cg1",
|
||||
"partition": "0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response in query range `table` format
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "service_instance_id",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "latency",
|
||||
"queryName": "latency",
|
||||
"isValueColumn": true
|
||||
},
|
||||
{
|
||||
"name": "throughput",
|
||||
"queryName": "throughput",
|
||||
"isValueColumn": true
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"client_id": "consumer-cg1-1",
|
||||
"latency": 48.99,
|
||||
"service_instance_id": "b0a851d7-1735-4e3f-8f5f-7c63a8a55a24",
|
||||
"service_name": "consumer-svc",
|
||||
"throughput": 14.97
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"client_id": "consumer-cg1-1",
|
||||
"latency": 25.21,
|
||||
"service_instance_id": "ccf49550-2e8f-4c7b-be29-b9e0891ef93d",
|
||||
"service_name": "consumer-svc",
|
||||
"throughput": 24.91
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -7,3 +7,10 @@ type MessagingQueue struct {
|
||||
End int64 `json:"end"`
|
||||
Variables map[string]string `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
type Clients struct {
|
||||
Hash map[string]struct{}
|
||||
ClientID []string
|
||||
ServiceInstanceID []string
|
||||
ServiceName []string
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ WITH consumer_query AS (
|
||||
GROUP BY serviceName
|
||||
)
|
||||
|
||||
-- Main query to select all metrics
|
||||
SELECT
|
||||
serviceName AS service_name,
|
||||
p99,
|
||||
@@ -65,7 +64,7 @@ SELECT
|
||||
serviceName AS service_name,
|
||||
p99,
|
||||
COALESCE((error_count * 100.0) / total_count, 0) AS error_percentage,
|
||||
COALESCE(total_count / %d, 0) AS rps -- Convert nanoseconds to seconds
|
||||
COALESCE(total_count / %d, 0) AS throughput -- Convert nanoseconds to seconds
|
||||
FROM
|
||||
producer_query
|
||||
ORDER BY
|
||||
@@ -74,3 +73,25 @@ ORDER BY
|
||||
`, start, end, queueType, topic, partition, timeRange)
|
||||
return query
|
||||
}
|
||||
|
||||
func generateNetworkLatencyThroughputSQL(start, end int64, consumerGroup, partitionID, queueType string) string {
|
||||
timeRange := (end - start) / 1000000000
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
stringTagMap['messaging.client_id'] AS client_id,
|
||||
stringTagMap['service.instance.id'] AS service_instance_id,
|
||||
serviceName AS service_name,
|
||||
count(*) / %d AS throughput
|
||||
FROM signoz_traces.distributed_signoz_index_v2
|
||||
WHERE
|
||||
timestamp >= '%d'
|
||||
AND timestamp <= '%d'
|
||||
AND kind = 5
|
||||
AND msgSystem = '%s'
|
||||
AND stringTagMap['messaging.kafka.consumer.group'] = '%s'
|
||||
AND stringTagMap['messaging.destination.partition.id'] = '%s'
|
||||
GROUP BY service_name, client_id, service_instance_id
|
||||
ORDER BY throughput DESC
|
||||
`, timeRange, start, end, queueType, consumerGroup, partitionID)
|
||||
return query
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package kafka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
@@ -35,6 +37,146 @@ func BuildQueryRangeParams(messagingQueue *MessagingQueue, queryContext string)
|
||||
return queryRangeParams, nil
|
||||
}
|
||||
|
||||
func buildClickHouseQueryNetwork(messagingQueue *MessagingQueue, queueType string) (*v3.ClickHouseQuery, error) {
|
||||
start := messagingQueue.Start
|
||||
end := messagingQueue.End
|
||||
consumerGroup, ok := messagingQueue.Variables["consumer_group"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("consumer_group not found in the request")
|
||||
}
|
||||
|
||||
partitionID, ok := messagingQueue.Variables["partition"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("partition not found in the request")
|
||||
}
|
||||
|
||||
query := generateNetworkLatencyThroughputSQL(start, end, consumerGroup, partitionID, queueType)
|
||||
|
||||
return &v3.ClickHouseQuery{
|
||||
Query: query,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func formatstring(str []string) string {
|
||||
joined := strings.Join(str, ", ")
|
||||
if len(joined) <= 2 {
|
||||
return ""
|
||||
}
|
||||
return joined[1 : len(joined)-1]
|
||||
}
|
||||
|
||||
func buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd int64, attributeCache *Clients) (map[string]*v3.BuilderQuery, error) {
|
||||
bq := make(map[string]*v3.BuilderQuery)
|
||||
queryName := fmt.Sprintf("latency")
|
||||
|
||||
chq := &v3.BuilderQuery{
|
||||
QueryName: queryName,
|
||||
StepInterval: common.MinAllowedStepInterval(unixMilliStart, unixMilliEnd),
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "kafka_consumer_fetch_latency_avg",
|
||||
},
|
||||
AggregateOperator: v3.AggregateOperatorAvg,
|
||||
Temporality: v3.Unspecified,
|
||||
TimeAggregation: v3.TimeAggregationAvg,
|
||||
SpaceAggregation: v3.SpaceAggregationAvg,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service_name",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorIn,
|
||||
Value: attributeCache.ServiceName,
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "client_id",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorIn,
|
||||
Value: attributeCache.ClientID,
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service_instance_id",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorIn,
|
||||
Value: attributeCache.ServiceInstanceID,
|
||||
},
|
||||
},
|
||||
},
|
||||
Expression: queryName,
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
GroupBy: []v3.AttributeKey{{
|
||||
Key: "service_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
{
|
||||
Key: "client_id",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
{
|
||||
Key: "service_instance_id",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
},
|
||||
}
|
||||
bq[queryName] = chq
|
||||
return bq, nil
|
||||
}
|
||||
|
||||
func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, attributeCache *Clients) (*v3.QueryRangeParamsV3, error) {
|
||||
|
||||
queueType := kafkaQueue
|
||||
|
||||
unixMilliStart := messagingQueue.Start / 1000000
|
||||
unixMilliEnd := messagingQueue.End / 1000000
|
||||
|
||||
var cq *v3.CompositeQuery
|
||||
|
||||
if queryContext == "throughput" {
|
||||
chq, err := buildClickHouseQueryNetwork(messagingQueue, queueType)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cq, err = buildCompositeQuery(chq, queryContext)
|
||||
|
||||
} else if queryContext == "fetch-latency" {
|
||||
bhq, err := buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd, attributeCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cq = &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: bhq,
|
||||
PanelType: v3.PanelTypeTable,
|
||||
}
|
||||
}
|
||||
|
||||
queryRangeParams := &v3.QueryRangeParamsV3{
|
||||
Start: unixMilliStart,
|
||||
End: unixMilliEnd,
|
||||
Step: defaultStepInterval,
|
||||
CompositeQuery: cq,
|
||||
Version: "v4",
|
||||
FormatForWeb: true,
|
||||
}
|
||||
|
||||
return queryRangeParams, nil
|
||||
}
|
||||
|
||||
func buildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, queryContext string) (*v3.ClickHouseQuery, error) {
|
||||
start := messagingQueue.Start
|
||||
end := messagingQueue.End
|
||||
@@ -48,15 +190,14 @@ func buildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer
|
||||
return nil, fmt.Errorf("invalid type for Partition")
|
||||
}
|
||||
|
||||
consumerGroup, ok := messagingQueue.Variables["consumer_group"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type for consumer group")
|
||||
}
|
||||
|
||||
var query string
|
||||
if queryContext == "producer" {
|
||||
query = generateProducerSQL(start, end, topic, partition, queueType)
|
||||
} else if queryContext == "consumer" {
|
||||
consumerGroup, ok := messagingQueue.Variables["consumer_group"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type for consumer group")
|
||||
}
|
||||
query = generateConsumerSQL(start, end, topic, partition, consumerGroup, queueType)
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ func PrepareLinksToTraces(ts time.Time, filterItems []v3.FilterItem) string {
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(urlData)
|
||||
compositeQuery := url.QueryEscape(string(data))
|
||||
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
|
||||
|
||||
optionsData, _ := json.Marshal(options)
|
||||
urlEncodedOptions := url.QueryEscape(string(optionsData))
|
||||
@@ -185,7 +185,7 @@ func PrepareLinksToLogs(ts time.Time, filterItems []v3.FilterItem) string {
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(urlData)
|
||||
compositeQuery := url.QueryEscape(string(data))
|
||||
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
|
||||
|
||||
optionsData, _ := json.Marshal(options)
|
||||
urlEncodedOptions := url.QueryEscape(string(optionsData))
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
package alertstov4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/rules"
|
||||
"go.uber.org/multierr"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var Version = "0.45-alerts-to-v4"
|
||||
|
||||
var mapTimeAggregation = map[v3.AggregateOperator]v3.TimeAggregation{
|
||||
v3.AggregateOperatorSum: v3.TimeAggregationSum,
|
||||
v3.AggregateOperatorMin: v3.TimeAggregationMin,
|
||||
v3.AggregateOperatorMax: v3.TimeAggregationMax,
|
||||
v3.AggregateOperatorSumRate: v3.TimeAggregationRate,
|
||||
v3.AggregateOperatorAvgRate: v3.TimeAggregationRate,
|
||||
v3.AggregateOperatorMinRate: v3.TimeAggregationRate,
|
||||
v3.AggregateOperatorMaxRate: v3.TimeAggregationRate,
|
||||
v3.AggregateOperatorHistQuant50: v3.TimeAggregationUnspecified,
|
||||
v3.AggregateOperatorHistQuant75: v3.TimeAggregationUnspecified,
|
||||
v3.AggregateOperatorHistQuant90: v3.TimeAggregationUnspecified,
|
||||
v3.AggregateOperatorHistQuant95: v3.TimeAggregationUnspecified,
|
||||
v3.AggregateOperatorHistQuant99: v3.TimeAggregationUnspecified,
|
||||
}
|
||||
|
||||
var mapSpaceAggregation = map[v3.AggregateOperator]v3.SpaceAggregation{
|
||||
v3.AggregateOperatorSum: v3.SpaceAggregationSum,
|
||||
v3.AggregateOperatorMin: v3.SpaceAggregationMin,
|
||||
v3.AggregateOperatorMax: v3.SpaceAggregationMax,
|
||||
v3.AggregateOperatorSumRate: v3.SpaceAggregationSum,
|
||||
v3.AggregateOperatorAvgRate: v3.SpaceAggregationAvg,
|
||||
v3.AggregateOperatorMinRate: v3.SpaceAggregationMin,
|
||||
v3.AggregateOperatorMaxRate: v3.SpaceAggregationMax,
|
||||
v3.AggregateOperatorHistQuant50: v3.SpaceAggregationPercentile50,
|
||||
v3.AggregateOperatorHistQuant75: v3.SpaceAggregationPercentile75,
|
||||
v3.AggregateOperatorHistQuant90: v3.SpaceAggregationPercentile90,
|
||||
v3.AggregateOperatorHistQuant95: v3.SpaceAggregationPercentile95,
|
||||
v3.AggregateOperatorHistQuant99: v3.SpaceAggregationPercentile99,
|
||||
}
|
||||
|
||||
func canMigrateOperator(operator v3.AggregateOperator) bool {
|
||||
switch operator {
|
||||
case v3.AggregateOperatorSum,
|
||||
v3.AggregateOperatorMin,
|
||||
v3.AggregateOperatorMax,
|
||||
v3.AggregateOperatorSumRate,
|
||||
v3.AggregateOperatorAvgRate,
|
||||
v3.AggregateOperatorMinRate,
|
||||
v3.AggregateOperatorMaxRate,
|
||||
v3.AggregateOperatorHistQuant50,
|
||||
v3.AggregateOperatorHistQuant75,
|
||||
v3.AggregateOperatorHistQuant90,
|
||||
v3.AggregateOperatorHistQuant95,
|
||||
v3.AggregateOperatorHistQuant99:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Migrate(conn *sqlx.DB) error {
|
||||
ruleDB := rules.NewRuleDB(conn)
|
||||
storedRules, err := ruleDB.GetStoredRules(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, storedRule := range storedRules {
|
||||
parsedRule, errs := rules.ParsePostableRule([]byte(storedRule.Data))
|
||||
if len(errs) > 0 {
|
||||
// this should not happen but if it does, we should not stop the migration
|
||||
zap.L().Error("Error parsing rule", zap.Error(multierr.Combine(errs...)), zap.Int("rule", storedRule.Id))
|
||||
continue
|
||||
}
|
||||
zap.L().Info("Rule parsed", zap.Int("rule", storedRule.Id))
|
||||
updated := false
|
||||
if parsedRule.RuleCondition != nil && parsedRule.Version == "" {
|
||||
if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
|
||||
// check if all the queries can be converted to v4
|
||||
canMigrate := true
|
||||
for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries {
|
||||
if query.DataSource == v3.DataSourceMetrics && query.Expression == query.QueryName {
|
||||
if !canMigrateOperator(query.AggregateOperator) {
|
||||
canMigrate = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if canMigrate {
|
||||
parsedRule.Version = "v4"
|
||||
for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries {
|
||||
if query.DataSource == v3.DataSourceMetrics && query.Expression == query.QueryName {
|
||||
// update aggregate attribute
|
||||
if query.AggregateOperator == v3.AggregateOperatorSum ||
|
||||
query.AggregateOperator == v3.AggregateOperatorMin ||
|
||||
query.AggregateOperator == v3.AggregateOperatorMax {
|
||||
query.AggregateAttribute.Type = "Gauge"
|
||||
}
|
||||
if query.AggregateOperator == v3.AggregateOperatorSumRate ||
|
||||
query.AggregateOperator == v3.AggregateOperatorAvgRate ||
|
||||
query.AggregateOperator == v3.AggregateOperatorMinRate ||
|
||||
query.AggregateOperator == v3.AggregateOperatorMaxRate {
|
||||
query.AggregateAttribute.Type = "Sum"
|
||||
}
|
||||
|
||||
if query.AggregateOperator == v3.AggregateOperatorHistQuant50 ||
|
||||
query.AggregateOperator == v3.AggregateOperatorHistQuant75 ||
|
||||
query.AggregateOperator == v3.AggregateOperatorHistQuant90 ||
|
||||
query.AggregateOperator == v3.AggregateOperatorHistQuant95 ||
|
||||
query.AggregateOperator == v3.AggregateOperatorHistQuant99 {
|
||||
query.AggregateAttribute.Type = "Histogram"
|
||||
}
|
||||
query.AggregateAttribute.DataType = v3.AttributeKeyDataTypeFloat64
|
||||
query.AggregateAttribute.IsColumn = true
|
||||
query.TimeAggregation = mapTimeAggregation[query.AggregateOperator]
|
||||
query.SpaceAggregation = mapSpaceAggregation[query.AggregateOperator]
|
||||
query.AggregateOperator = v3.AggregateOperator(query.TimeAggregation)
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !updated {
|
||||
zap.L().Info("Rule not updated", zap.Int("rule", storedRule.Id))
|
||||
continue
|
||||
}
|
||||
|
||||
ruleJSON, jsonErr := json.Marshal(parsedRule)
|
||||
if jsonErr != nil {
|
||||
zap.L().Error("Error marshalling rule; skipping rule migration", zap.Error(jsonErr), zap.Int("rule", storedRule.Id))
|
||||
continue
|
||||
}
|
||||
|
||||
stmt, prepareError := conn.PrepareContext(context.Background(), `UPDATE rules SET data=$3 WHERE id=$4;`)
|
||||
if prepareError != nil {
|
||||
zap.L().Error("Error in preparing statement for UPDATE to rules", zap.Error(prepareError))
|
||||
continue
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.Exec(ruleJSON, storedRule.Id); err != nil {
|
||||
zap.L().Error("Error in Executing prepared statement for UPDATE to rules", zap.Error(err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package alertscustomstep
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/rules"
|
||||
"go.uber.org/multierr"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var Version = "0.47-alerts-custom-step"
|
||||
|
||||
func Migrate(conn *sqlx.DB) error {
|
||||
ruleDB := rules.NewRuleDB(conn)
|
||||
storedRules, err := ruleDB.GetStoredRules(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, storedRule := range storedRules {
|
||||
parsedRule, errs := rules.ParsePostableRule([]byte(storedRule.Data))
|
||||
if len(errs) > 0 {
|
||||
// this should not happen but if it does, we should not stop the migration
|
||||
zap.L().Error("Error parsing rule", zap.Error(multierr.Combine(errs...)), zap.Int("rule", storedRule.Id))
|
||||
continue
|
||||
}
|
||||
zap.L().Info("Rule parsed", zap.Int("rule", storedRule.Id))
|
||||
updated := false
|
||||
if parsedRule.RuleCondition != nil {
|
||||
if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
|
||||
if parsedRule.EvalWindow <= rules.Duration(6*time.Hour) {
|
||||
for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries {
|
||||
if query.StepInterval > 60 {
|
||||
updated = true
|
||||
zap.L().Info("Updating step interval", zap.Int("rule", storedRule.Id), zap.Int64("old", query.StepInterval), zap.Int64("new", 60))
|
||||
query.StepInterval = 60
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !updated {
|
||||
zap.L().Info("Rule not updated", zap.Int("rule", storedRule.Id))
|
||||
continue
|
||||
}
|
||||
|
||||
ruleJSON, jsonErr := json.Marshal(parsedRule)
|
||||
if jsonErr != nil {
|
||||
zap.L().Error("Error marshalling rule; skipping rule migration", zap.Error(jsonErr), zap.Int("rule", storedRule.Id))
|
||||
continue
|
||||
}
|
||||
|
||||
stmt, prepareError := conn.PrepareContext(context.Background(), `UPDATE rules SET data=$3 WHERE id=$4;`)
|
||||
if prepareError != nil {
|
||||
zap.L().Error("Error in preparing statement for UPDATE to rules", zap.Error(prepareError))
|
||||
continue
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.Exec(ruleJSON, storedRule.Id); err != nil {
|
||||
zap.L().Error("Error in Executing prepared statement for UPDATE to rules", zap.Error(err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -7,9 +7,6 @@ import (
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
"github.com/jmoiron/sqlx"
|
||||
alertstov4 "go.signoz.io/signoz/pkg/query-service/migrate/0_45_alerts_to_v4"
|
||||
alertscustomstep "go.signoz.io/signoz/pkg/query-service/migrate/0_47_alerts_custom_step"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type DataMigration struct {
|
||||
@@ -56,28 +53,6 @@ func Migrate(dsn string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if m, err := getMigrationVersion(conn, "0.45_alerts_to_v4"); err == nil && m == nil {
|
||||
if err := alertstov4.Migrate(conn); err != nil {
|
||||
zap.L().Error("failed to migrate 0.45_alerts_to_v4", zap.Error(err))
|
||||
} else {
|
||||
_, err := conn.Exec("INSERT INTO data_migrations (version, succeeded) VALUES ('0.45_alerts_to_v4', true)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m, err := getMigrationVersion(conn, "0.47_alerts_custom_step"); err == nil && m == nil {
|
||||
if err := alertscustomstep.Migrate(conn); err != nil {
|
||||
zap.L().Error("failed to migrate 0.47_alerts_custom_step", zap.Error(err))
|
||||
} else {
|
||||
_, err := conn.Exec("INSERT INTO data_migrations (version, succeeded) VALUES ('0.47_alerts_custom_step', true)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,35 @@ func (s AlertState) String() string {
|
||||
panic(errors.Errorf("unknown alert state: %d", s))
|
||||
}
|
||||
|
||||
func (s AlertState) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
func (s *AlertState) UnmarshalJSON(b []byte) error {
|
||||
var v interface{}
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
switch value := v.(type) {
|
||||
case string:
|
||||
switch value {
|
||||
case "inactive":
|
||||
*s = StateInactive
|
||||
case "pending":
|
||||
*s = StatePending
|
||||
case "firing":
|
||||
*s = StateFiring
|
||||
case "disabled":
|
||||
*s = StateDisabled
|
||||
default:
|
||||
return errors.New("invalid alert state")
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid alert state")
|
||||
}
|
||||
}
|
||||
|
||||
type Alert struct {
|
||||
State AlertState
|
||||
|
||||
|
||||
@@ -16,6 +16,22 @@ import (
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type AlertType string
|
||||
|
||||
const (
|
||||
AlertTypeMetric AlertType = "METRIC_BASED_ALERT"
|
||||
AlertTypeTraces AlertType = "TRACES_BASED_ALERT"
|
||||
AlertTypeLogs AlertType = "LOGS_BASED_ALERT"
|
||||
AlertTypeExceptions AlertType = "EXCEPTIONS_BASED_ALERT"
|
||||
)
|
||||
|
||||
type RuleDataKind string
|
||||
|
||||
const (
|
||||
RuleDataKindJson RuleDataKind = "json"
|
||||
RuleDataKindYaml RuleDataKind = "yaml"
|
||||
)
|
||||
|
||||
// this file contains api request and responses to be
|
||||
// served over http
|
||||
|
||||
@@ -31,12 +47,12 @@ func newApiErrorBadData(err error) *model.ApiError {
|
||||
|
||||
// PostableRule is used to create alerting rule from HTTP api
|
||||
type PostableRule struct {
|
||||
AlertName string `yaml:"alert,omitempty" json:"alert,omitempty"`
|
||||
AlertType string `yaml:"alertType,omitempty" json:"alertType,omitempty"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"`
|
||||
EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"`
|
||||
Frequency Duration `yaml:"frequency,omitempty" json:"frequency,omitempty"`
|
||||
AlertName string `yaml:"alert,omitempty" json:"alert,omitempty"`
|
||||
AlertType AlertType `yaml:"alertType,omitempty" json:"alertType,omitempty"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"`
|
||||
EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"`
|
||||
Frequency Duration `yaml:"frequency,omitempty" json:"frequency,omitempty"`
|
||||
|
||||
RuleCondition *RuleCondition `yaml:"condition,omitempty" json:"condition,omitempty"`
|
||||
Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
|
||||
@@ -234,8 +250,8 @@ type GettableRules struct {
|
||||
|
||||
// GettableRule has info for an alerting rules.
|
||||
type GettableRule struct {
|
||||
Id string `json:"id"`
|
||||
State string `json:"state"`
|
||||
Id string `json:"id"`
|
||||
State AlertState `json:"state"`
|
||||
PostableRule
|
||||
CreatedAt *time.Time `json:"createAt"`
|
||||
CreatedBy *string `json:"createBy"`
|
||||
|
||||
@@ -325,9 +325,9 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
|
||||
continue
|
||||
}
|
||||
alertNames = append(alertNames, rule.AlertName)
|
||||
if rule.AlertType == "LOGS_BASED_ALERT" {
|
||||
if rule.AlertType == AlertTypeLogs {
|
||||
alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1
|
||||
} else if rule.AlertType == "METRIC_BASED_ALERT" {
|
||||
} else if rule.AlertType == AlertTypeMetric {
|
||||
alertsInfo.MetricBasedAlerts = alertsInfo.MetricBasedAlerts + 1
|
||||
if rule.RuleCondition != nil && rule.RuleCondition.CompositeQuery != nil {
|
||||
if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
||||
@@ -343,7 +343,7 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if rule.AlertType == "TRACES_BASED_ALERT" {
|
||||
} else if rule.AlertType == AlertTypeTraces {
|
||||
alertsInfo.TracesBasedAlerts = alertsInfo.TracesBasedAlerts + 1
|
||||
}
|
||||
alertsInfo.TotalAlerts = alertsInfo.TotalAlerts + 1
|
||||
|
||||
@@ -20,11 +20,9 @@ import (
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// opentracing "github.com/opentracing/opentracing-go"
|
||||
am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
|
||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils/labels"
|
||||
)
|
||||
@@ -240,20 +238,6 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error
|
||||
|
||||
parsedRule, errs := ParsePostableRule([]byte(ruleStr))
|
||||
|
||||
currentRule, err := m.GetRule(ctx, id)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to get the rule from rule db", zap.String("id", id), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if !checkIfTraceOrLogQB(¤tRule.PostableRule) {
|
||||
// check if the new rule uses any feature that is not enabled
|
||||
err = m.checkFeatureUsage(parsedRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
zap.L().Error("failed to parse rules", zap.Errors("errors", errs))
|
||||
// just one rule is being parsed so expect just one error
|
||||
@@ -272,20 +256,6 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error
|
||||
}
|
||||
}
|
||||
|
||||
// update feature usage if the current rule is not a trace or log query builder
|
||||
if !checkIfTraceOrLogQB(¤tRule.PostableRule) {
|
||||
err = m.updateFeatureUsage(parsedRule, 1)
|
||||
if err != nil {
|
||||
zap.L().Error("error updating feature usage", zap.Error(err))
|
||||
}
|
||||
// update feature usage if the new rule is not a trace or log query builder and the current rule is
|
||||
} else if !checkIfTraceOrLogQB(parsedRule) {
|
||||
err = m.updateFeatureUsage(¤tRule.PostableRule, -1)
|
||||
if err != nil {
|
||||
zap.L().Error("error updating feature usage", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -335,13 +305,6 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error {
|
||||
return fmt.Errorf("delete rule received an rule id in invalid format, must be a number")
|
||||
}
|
||||
|
||||
// update feature usage
|
||||
rule, err := m.GetRule(ctx, id)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to get the rule from rule db", zap.String("id", id), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
taskName := prepareTaskName(int64(idInt))
|
||||
if !m.opts.DisableRules {
|
||||
m.deleteTask(taskName)
|
||||
@@ -352,11 +315,6 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateFeatureUsage(&rule.PostableRule, -1)
|
||||
if err != nil {
|
||||
zap.L().Error("error updating feature usage", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -381,12 +339,6 @@ func (m *Manager) deleteTask(taskName string) {
|
||||
func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule, error) {
|
||||
parsedRule, errs := ParsePostableRule([]byte(ruleStr))
|
||||
|
||||
// check if the rule uses any feature that is not enabled
|
||||
err := m.checkFeatureUsage(parsedRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
zap.L().Error("failed to parse rules", zap.Errors("errors", errs))
|
||||
// just one rule is being parsed so expect just one error
|
||||
@@ -409,11 +361,6 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update feature usage
|
||||
err = m.updateFeatureUsage(parsedRule, 1)
|
||||
if err != nil {
|
||||
zap.L().Error("error updating feature usage", zap.Error(err))
|
||||
}
|
||||
gettableRule := &GettableRule{
|
||||
Id: fmt.Sprintf("%d", lastInsertId),
|
||||
PostableRule: *parsedRule,
|
||||
@@ -421,59 +368,6 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule
|
||||
return gettableRule, nil
|
||||
}
|
||||
|
||||
func (m *Manager) updateFeatureUsage(parsedRule *PostableRule, usage int64) error {
|
||||
isTraceOrLogQB := checkIfTraceOrLogQB(parsedRule)
|
||||
if isTraceOrLogQB {
|
||||
feature, err := m.featureFlags.GetFeatureFlag(model.QueryBuilderAlerts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
feature.Usage += usage
|
||||
if feature.Usage == feature.UsageLimit && feature.UsageLimit != -1 {
|
||||
feature.Active = false
|
||||
}
|
||||
if feature.Usage < feature.UsageLimit || feature.UsageLimit == -1 {
|
||||
feature.Active = true
|
||||
}
|
||||
err = m.featureFlags.UpdateFeatureFlag(feature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) checkFeatureUsage(parsedRule *PostableRule) error {
|
||||
isTraceOrLogQB := checkIfTraceOrLogQB(parsedRule)
|
||||
if isTraceOrLogQB {
|
||||
err := m.featureFlags.CheckFeature(model.QueryBuilderAlerts)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case model.ErrFeatureUnavailable:
|
||||
zap.L().Error("feature unavailable", zap.String("featureKey", model.QueryBuilderAlerts), zap.Error(err))
|
||||
return model.BadRequest(err)
|
||||
default:
|
||||
zap.L().Error("feature check failed", zap.String("featureKey", model.QueryBuilderAlerts), zap.Error(err))
|
||||
return model.BadRequest(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIfTraceOrLogQB(parsedRule *PostableRule) bool {
|
||||
if parsedRule != nil {
|
||||
if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder {
|
||||
for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries {
|
||||
if query.DataSource == v3.DataSourceTraces || query.DataSource == v3.DataSourceLogs {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) addTask(rule *PostableRule, taskName string) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
@@ -569,7 +463,7 @@ func (m *Manager) prepareTask(acquireLock bool, r *PostableRule, taskName string
|
||||
m.rules[ruleId] = pr
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf(fmt.Sprintf("unsupported rule type. Supported types: %s, %s", RuleTypeProm, RuleTypeThreshold))
|
||||
return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", RuleTypeProm, RuleTypeThreshold)
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -710,10 +604,10 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*GettableRules, error) {
|
||||
|
||||
// fetch state of rule from memory
|
||||
if rm, ok := m.rules[ruleResponse.Id]; !ok {
|
||||
ruleResponse.State = StateDisabled.String()
|
||||
ruleResponse.State = StateDisabled
|
||||
ruleResponse.Disabled = true
|
||||
} else {
|
||||
ruleResponse.State = rm.State().String()
|
||||
ruleResponse.State = rm.State()
|
||||
}
|
||||
ruleResponse.CreatedAt = s.CreatedAt
|
||||
ruleResponse.CreatedBy = s.CreatedBy
|
||||
@@ -737,10 +631,10 @@ func (m *Manager) GetRule(ctx context.Context, id string) (*GettableRule, error)
|
||||
r.Id = fmt.Sprintf("%d", s.Id)
|
||||
// fetch state of rule from memory
|
||||
if rm, ok := m.rules[r.Id]; !ok {
|
||||
r.State = StateDisabled.String()
|
||||
r.State = StateDisabled
|
||||
r.Disabled = true
|
||||
} else {
|
||||
r.State = rm.State().String()
|
||||
r.State = rm.State()
|
||||
}
|
||||
r.CreatedAt = s.CreatedAt
|
||||
r.CreatedBy = s.CreatedBy
|
||||
@@ -846,10 +740,10 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleId string)
|
||||
|
||||
// fetch state of rule from memory
|
||||
if rm, ok := m.rules[ruleId]; !ok {
|
||||
response.State = StateDisabled.String()
|
||||
response.State = StateDisabled
|
||||
response.Disabled = true
|
||||
} else {
|
||||
response.State = rm.State().String()
|
||||
response.State = rm.State()
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
|
||||
@@ -91,8 +91,7 @@ type ThresholdRule struct {
|
||||
lastTimestampWithDatapoints time.Time
|
||||
|
||||
// Type of the rule
|
||||
// One of ["LOGS_BASED_ALERT", "TRACES_BASED_ALERT", "METRIC_BASED_ALERT", "EXCEPTIONS_BASED_ALERT"]
|
||||
typ string
|
||||
typ AlertType
|
||||
|
||||
// querier is used for alerts created before the introduction of new metrics query builder
|
||||
querier interfaces.Querier
|
||||
@@ -671,7 +670,7 @@ func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) str
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(urlData)
|
||||
compositeQuery := url.QueryEscape(string(data))
|
||||
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
|
||||
|
||||
optionsData, _ := json.Marshal(options)
|
||||
urlEncodedOptions := url.QueryEscape(string(optionsData))
|
||||
@@ -735,7 +734,7 @@ func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) s
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(urlData)
|
||||
compositeQuery := url.QueryEscape(string(data))
|
||||
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
|
||||
|
||||
optionsData, _ := json.Marshal(options)
|
||||
urlEncodedOptions := url.QueryEscape(string(optionsData))
|
||||
@@ -975,12 +974,12 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie
|
||||
// Links with timestamps should go in annotations since labels
|
||||
// is used alert grouping, and we want to group alerts with the same
|
||||
// label set, but different timestamps, together.
|
||||
if r.typ == "TRACES_BASED_ALERT" {
|
||||
if r.typ == AlertTypeTraces {
|
||||
link := r.prepareLinksToTraces(ts, smpl.MetricOrig)
|
||||
if link != "" && r.hostFromSource() != "" {
|
||||
annotations = append(annotations, labels.Label{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)})
|
||||
}
|
||||
} else if r.typ == "LOGS_BASED_ALERT" {
|
||||
} else if r.typ == AlertTypeLogs {
|
||||
link := r.prepareLinksToLogs(ts, smpl.MetricOrig)
|
||||
if link != "" && r.hostFromSource() != "" {
|
||||
annotations = append(annotations, labels.Label{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)})
|
||||
|
||||
@@ -674,7 +674,7 @@ func TestNormalizeLabelName(t *testing.T) {
|
||||
func TestPrepareLinksToLogs(t *testing.T) {
|
||||
postableRule := PostableRule{
|
||||
AlertName: "Tricky Condition Tests",
|
||||
AlertType: "LOGS_BASED_ALERT",
|
||||
AlertType: AlertTypeLogs,
|
||||
RuleType: RuleTypeThreshold,
|
||||
EvalWindow: Duration(5 * time.Minute),
|
||||
Frequency: Duration(1 * time.Minute),
|
||||
|
||||
@@ -649,12 +649,12 @@
|
||||
|
||||
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
|
||||
-->
|
||||
<!--
|
||||
|
||||
<macros>
|
||||
<shard>01</shard>
|
||||
<replica>example01-01-1</replica>
|
||||
</macros>
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->
|
||||
|
||||
@@ -192,7 +192,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -205,7 +205,7 @@ services:
|
||||
# condition: service_healthy
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.102.2
|
||||
image: signoz/signoz-otel-collector:0.102.7
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user