Compare commits

..

1 Commits

Author SHA1 Message Date
Yunus M
42af34544f feat: move anomaly detection behind ff and show beta 2024-10-14 11:08:21 +05:30
159 changed files with 3344 additions and 6913 deletions

View File

@@ -31,7 +31,6 @@ jobs:
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
KAFKA_SPAN_EVAL: true
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"

View File

@@ -79,7 +79,7 @@ build-query-service-static:
@if [ $(DEV_BUILD) != "" ]; then \
cd $(QUERY_SERVICE_DIRECTORY) && \
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \
else \
cd $(QUERY_SERVICE_DIRECTORY) && \
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
@@ -188,4 +188,13 @@ check-no-ee-references:
fi
test:
go test ./pkg/query-service/...
go test ./pkg/query-service/app/metrics/...
go test ./pkg/query-service/cache/...
go test ./pkg/query-service/app/...
go test ./pkg/query-service/app/querier/...
go test ./pkg/query-service/converter/...
go test ./pkg/query-service/formatter/...
go test ./pkg/query-service/tests/integration/...
go test ./pkg/query-service/rules/...
go test ./pkg/query-service/collectorsimulator/...
go test ./pkg/query-service/postprocess/...

View File

@@ -133,7 +133,7 @@ services:
# - ./data/clickhouse-3/:/var/lib/clickhouse/
alertmanager:
image: signoz/alertmanager:0.23.7
image: signoz/alertmanager:0.23.5
volumes:
- ./data/alertmanager:/data
command:
@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.56.0
image: signoz/query-service:0.55.0
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.56.0
image: signoz/frontend:0.55.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.12
image: signoz/signoz-otel-collector:0.102.10
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -131,8 +131,8 @@ processors:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${env:DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
@@ -142,7 +142,7 @@ exporters:
# logging: {}
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${env:DOCKER_MULTI_NODE_CLUSTER}
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
use_new_schema: true
extensions:

View File

@@ -57,7 +57,7 @@ services:
alertmanager:
container_name: signoz-alertmanager
image: signoz/alertmanager:0.23.7
image: signoz/alertmanager:0.23.5
volumes:
- ./data/alertmanager:/data
depends_on:
@@ -84,7 +84,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.12
image: signoz/signoz-otel-collector:0.102.10
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -34,7 +34,7 @@ x-db-depend: &db-depend
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator-sync:
otel-collector-migrator:
condition: service_completed_successfully
# clickhouse-2:
# condition: service_healthy
@@ -147,7 +147,7 @@ services:
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
container_name: signoz-alertmanager
volumes:
- ./data/alertmanager:/data
@@ -162,7 +162,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.56.0}
image: signoz/query-service:${DOCKER_TAG:-0.55.0}
container_name: signoz-query-service
command:
[
@@ -201,7 +201,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
image: signoz/frontend:${DOCKER_TAG:-0.55.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -212,13 +212,11 @@ services:
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator-sync:
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
container_name: otel-migrator-sync
container_name: otel-migrator
command:
- "sync"
- "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on:
clickhouse:
condition: service_healthy
@@ -227,25 +225,9 @@ services:
# clickhouse-3:
# condition: service_healthy
otel-collector-migrator-async:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
container_name: otel-migrator-async
command:
- "async"
- "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator-sync:
condition: service_completed_successfully
# clickhouse-2:
# condition: service_healthy
# clickhouse-3:
# condition: service_healthy
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.12}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.10}
container_name: signoz-otel-collector
command:
[
@@ -280,7 +262,7 @@ services:
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator-sync:
otel-collector-migrator:
condition: service_completed_successfully
query-service:
condition: service_healthy

View File

@@ -152,7 +152,7 @@ services:
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
container_name: signoz-alertmanager
volumes:
- ./data/alertmanager:/data
@@ -167,7 +167,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.56.0}
image: signoz/query-service:${DOCKER_TAG:-0.55.0}
container_name: signoz-query-service
command:
[
@@ -207,7 +207,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
image: signoz/frontend:${DOCKER_TAG:-0.55.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -233,7 +233,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.12}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.10}
container_name: signoz-otel-collector
command:
[

View File

@@ -142,8 +142,8 @@ extensions:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${env:DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
@@ -152,7 +152,7 @@ exporters:
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${env:DOCKER_MULTI_NODE_CLUSTER}
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
use_new_schema: true
# logging: {}

View File

@@ -9,15 +9,7 @@ import (
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
validPath := false
for _, allowedPrefix := range gateway.AllowedPrefix {
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
validPath = true
break
}
}
if !validPath {
if !strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+gateway.AllowedPrefix) {
rw.WriteHeader(http.StatusNotFound)
return
}

View File

@@ -53,11 +53,7 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
if anomalyQueryExists {
// ensure all queries have metric data source, and there should be only one anomaly query
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
// What is query.QueryName == query.Expression doing here?
// In the current implementation, the way to recognize if a query is a formula is by
// checking if the expression is the same as the query name. if the expression is different
// then it is a formula. otherwise, it is simple builder query.
if query.DataSource != v3.DataSourceMetrics && query.QueryName == query.Expression {
if query.DataSource != v3.DataSourceMetrics {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("all queries must have metric data source")}, nil)
return
}
@@ -104,13 +100,6 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
anomaly.WithReader[*anomaly.HourlyProvider](aH.opts.DataConnector),
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](aH.opts.FeatureFlags),
)
default:
provider = anomaly.NewDailyProvider(
anomaly.WithCache[*anomaly.DailyProvider](aH.opts.Cache),
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.DailyProvider](aH.opts.DataConnector),
anomaly.WithFeatureLookup[*anomaly.DailyProvider](aH.opts.FeatureFlags),
)
}
anomalies, err := provider.GetAnomalies(r.Context(), &anomaly.GetAnomaliesRequest{Params: queryRangeParams})
if err != nil {

View File

@@ -8,9 +8,9 @@ import (
"strings"
)
var (
const (
RoutePrefix string = "/api/gateway"
AllowedPrefix []string = []string{"/v1/workspaces/me", "/v2/profiles/me"}
AllowedPrefix string = "/v1/workspaces/me"
)
type proxy struct {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -1 +0,0 @@
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}

View File

@@ -7,7 +7,6 @@ const create = async (
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.post('/rules', {
...props.data,
version: 'v4',
});
return {

View File

@@ -1,18 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllOrgPreferences = async (): Promise<
SuccessResponse<GetAllOrgPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/org/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllOrgPreferences;

View File

@@ -1,18 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllUserPreferences = async (): Promise<
SuccessResponse<GetAllUserPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllUserPreferences;

View File

@@ -1,20 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getOrgPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<SuccessResponse<GetOrgPreferenceResponseProps> | ErrorResponse> => {
const response = await axios.get(`/org/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getOrgPreference;

View File

@@ -1,22 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getUserPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<
SuccessResponse<GetUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getUserPreference;

View File

@@ -1,25 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateOrgPreferenceProps,
UpdateOrgPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateOrgPreference = async (
preferencePayload: UpdateOrgPreferenceProps,
): Promise<
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(`/org/preferences`, {
preference_value: preferencePayload.value,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateOrgPreference;

View File

@@ -1,25 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateUserPreferenceProps,
UpdateUserPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateUserPreference = async (
preferencePayload: UpdateUserPreferenceProps,
): Promise<
SuccessResponse<UpdateUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(`/user/preferences`, {
preference_value: preferencePayload.value,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateUserPreference;

View File

@@ -1,3 +1,46 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { AlertDef } from 'types/api/alerts/def';
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
export const chartHelpMessage = (
selectedDashboard: Dashboard | undefined,
graphType: PANEL_TYPES,
): string => `
Hi Team,
I need help in creating this chart. Here are my dashboard details
Name: ${selectedDashboard?.data.title || ''}
Panel type: ${graphType}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardHelpMessage = (
data: DashboardData | undefined,
selectedDashboard: Dashboard | undefined,
): string => `
Hi Team,
I need help with this dashboard. Here are my dashboard details
Name: ${data?.title || ''}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardListMessage = `Hi Team,
I need help with dashboards.
Thanks`;
export const listAlertMessage = `Hi Team,
I need help with managing alerts.
Thanks`;
export const onboardingHelpMessage = (
dataSourceName: string,
moduleId: string,
@@ -12,3 +55,35 @@ Module: ${moduleId}
Thanks
`;
export const alertHelpMessage = (
alertDef: AlertDef,
ruleId: number,
): string => `
Hi Team,
I need help in configuring this alert. Here are my alert rule details
Name: ${alertDef?.alert || ''}
Alert Type: ${alertDef?.alertType || ''}
State: ${(alertDef as any)?.state || ''}
Alert Id: ${ruleId}
Thanks`;
export const integrationsListMessage = `Hi Team,
I need help with Integrations.
Thanks`;
export const integrationDetailMessage = (
selectedIntegration: string,
): string => `
Hi Team,
I need help in configuring this integration.
Integration Id: ${selectedIntegration}
Thanks`;

View File

@@ -129,7 +129,6 @@ function LogDetail({
return (
<Drawer
width="60%"
maskStyle={{ background: 'none' }}
title={
<>
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />

View File

@@ -195,20 +195,21 @@ function ListLogView({
return (
<>
<Container
$isActiveLog={
isHighlighted ||
activeLog?.id === logData.id ||
activeContextLog?.id === logData.id
}
$isActiveLog={isHighlighted}
$isDarkMode={isDarkMode}
$logType={logType}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleDetailedView}
fontSize={fontSize}
>
<div className="log-line">
<LogStateIndicator type={logType} fontSize={fontSize} />
<LogStateIndicator
type={logType}
isActive={
activeLog?.id === logData.id || activeContextLog?.id === logData.id
}
fontSize={fontSize}
/>
<div>
<LogContainer fontSize={fontSize}>
<LogGeneralField

View File

@@ -1,8 +1,8 @@
/* eslint-disable no-nested-ternary */
import { Color } from '@signozhq/design-tokens';
import { Card, Typography } from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
import { getActiveLogBackground } from 'utils/logs';
interface LogTextProps {
linesPerRow?: number;
@@ -15,7 +15,6 @@ interface LogContainerProps {
export const Container = styled(Card)<{
$isActiveLog: boolean;
$isDarkMode: boolean;
$logType: string;
fontSize: FontSize;
}>`
width: 100% !important;
@@ -42,8 +41,13 @@ export const Container = styled(Card)<{
? `padding:0.3rem 0.6rem;`
: ``}
${({ $isActiveLog, $isDarkMode, $logType }): string =>
getActiveLogBackground($isActiveLog, $isDarkMode, $logType)}
${({ $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
: ''}
}
`;
export const Text = styled(Typography.Text)`

View File

@@ -41,4 +41,10 @@
background-color: var(--bg-sakura-500);
}
}
&.isActive {
.line {
background-color: var(--bg-robin-400, #7190f9);
}
}
}

View File

@@ -17,6 +17,14 @@ describe('LogStateIndicator', () => {
);
});
it('renders correctly when isActive is true', () => {
const { container } = render(
<LogStateIndicator type="INFO" isActive fontSize={FontSize.MEDIUM} />,
);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('isActive')).toBe(true);
});
it('renders correctly with different types', () => {
const { container: containerInfo } = render(
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,

View File

@@ -44,16 +44,22 @@ export const LogType = {
function LogStateIndicator({
type,
isActive,
fontSize,
}: {
type: string;
fontSize: FontSize;
isActive?: boolean;
}): JSX.Element {
return (
<div className="log-state-indicator">
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
<div className={cx('line', type, fontSize)}> </div>
</div>
);
}
LogStateIndicator.defaultProps = {
isActive: false,
};
export default LogStateIndicator;

View File

@@ -162,15 +162,20 @@ function RawLogView({
$isDarkMode={isDarkMode}
$isReadOnly={isReadOnly}
$isHightlightedLog={isHighlighted}
$isActiveLog={
activeLog?.id === data.id || activeContextLog?.id === data.id || isActiveLog
}
$logType={logType}
$isActiveLog={isActiveLog}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
fontSize={fontSize}
>
<LogStateIndicator type={logType} fontSize={fontSize} />
<LogStateIndicator
type={logType}
isActive={
activeLog?.id === data.id ||
activeContextLog?.id === data.id ||
isActiveLog
}
fontSize={fontSize}
/>
<RawLogContent
$isReadOnly={isReadOnly}

View File

@@ -13,7 +13,6 @@ export const RawLogViewContainer = styled(Row)<{
$isReadOnly?: boolean;
$isActiveLog?: boolean;
$isHightlightedLog: boolean;
$logType: string;
fontSize: FontSize;
}>`
position: relative;
@@ -35,12 +34,11 @@ export const RawLogViewContainer = styled(Row)<{
: `margin: 2px 0;`}
}
${({ $isActiveLog, $logType }): string =>
getActiveLogBackground($isActiveLog, true, $logType)}
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
? getActiveLogBackground($isActiveLog, $isDarkMode)
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
${({ $isHightlightedLog, $isDarkMode }): string =>

View File

@@ -35,6 +35,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
linesPerRow,
fontSize,
appendTo = 'center',
activeContextLog,
activeLog,
isListViewPanel,
} = props;
@@ -88,6 +90,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
<div className="table-timestamp">
<LogStateIndicator
type={getLogIndicatorTypeForTable(item)}
isActive={
activeLog?.id === item.id || activeContextLog?.id === item.id
}
fontSize={fontSize}
/>
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
@@ -125,7 +130,16 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
},
...(appendTo === 'end' ? fieldColumns : []),
];
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
}, [
fields,
isListViewPanel,
appendTo,
isDarkMode,
linesPerRow,
activeLog?.id,
activeContextLog?.id,
fontSize,
]);
return { columns, dataSource: flattenLogData };
};

View File

@@ -107,7 +107,6 @@ function DynamicColumnTable({
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
data-testid="additional-filters-button"
/>
</Dropdown>
)}

View File

@@ -3,6 +3,10 @@ import { QueryFunctionsTypes } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
{
value: QueryFunctionsTypes.ANOMALY,
label: 'Anomaly',
},
{
value: QueryFunctionsTypes.CUTOFF_MIN,
label: 'Cut Off Min',

View File

@@ -32,7 +32,7 @@
flex-direction: column;
gap: 8px;
width: 240px;
padding: 0px 8px;
padding: 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list {
@@ -41,10 +41,6 @@
gap: 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list-search {
margin-bottom: 16px;
}
.anomaly-alert-evaluation-view-series-list-title {
margin-top: 12px;
font-size: 13px !important;
@@ -63,32 +59,8 @@
flex-direction: row;
gap: 8px;
.anomaly-alert-evaluation-view-series-list-item-color {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-flex;
margin-right: 8px;
vertical-align: middle;
}
cursor: pointer;
}
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
}
@@ -118,63 +90,3 @@
}
}
}
.uplot-tooltip {
background-color: rgba(0, 0, 0, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #ddd;
font-size: 13px;
line-height: 1.4;
padding: 8px 12px;
pointer-events: none;
position: absolute;
z-index: 100;
max-height: 500px;
width: 280px;
overflow-y: auto;
display: none; /* Hide tooltip by default */
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.uplot-tooltip-title {
font-weight: bold;
margin-bottom: 4px;
}
.uplot-tooltip-series {
display: flex;
gap: 4px;
padding: 4px 0px;
align-items: center;
}
.uplot-tooltip-series-name {
margin-right: 4px;
}
.uplot-tooltip-band {
font-style: italic;
color: #666;
}
.uplot-tooltip-marker {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}

View File

@@ -2,9 +2,7 @@ import 'uplot/dist/uPlot.min.css';
import './AnomalyAlertEvaluationView.styles.scss';
import { Checkbox, Typography } from 'antd';
import Search from 'antd/es/input/Search';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useResizeObserver } from 'hooks/useDimensions';
import getAxes from 'lib/uPlotLib/utils/getAxes';
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
@@ -13,8 +11,6 @@ import { LineChart } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import uPlot from 'uplot';
import tooltipPlugin from './tooltipPlugin';
function UplotChart({
data,
options,
@@ -67,20 +63,13 @@ function AnomalyAlertEvaluationView({
const [seriesData, setSeriesData] = useState<any>({});
const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
const [filteredSeriesKeys, setFilteredSeriesKeys] = useState<string[]>([]);
const [allSeries, setAllSeries] = useState<string[]>([]);
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
useEffect(() => {
const chartData = getUplotChartDataForAnomalyDetection(data, isDarkMode);
const chartData = getUplotChartDataForAnomalyDetection(data);
setSeriesData(chartData);
setAllSeries(Object.keys(chartData));
setFilteredSeriesKeys(Object.keys(chartData));
}, [data, isDarkMode]);
}, [data]);
useEffect(() => {
const seriesKeys = Object.keys(seriesData);
@@ -141,6 +130,8 @@ function AnomalyAlertEvaluationView({
},
};
const allSeries = Object.keys(seriesData);
const initialData = allSeries.length
? [
seriesData[allSeries[0]].data[0], // Shared X-axis
@@ -151,38 +142,10 @@ function AnomalyAlertEvaluationView({
const options = {
width: dimensions.width,
height: dimensions.height - 36,
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
plugins: [bandsPlugin],
focus: {
alpha: 0.3,
},
legend: {
show: true,
live: false,
isolate: true,
},
cursor: {
lock: false,
focus: {
prox: 1e6,
bias: 1,
},
points: {
size: (
u: { series: { [x: string]: { points: { size: number } } } },
seriesIdx: string | number,
): number => u.series[seriesIdx].points.size * 3,
width: (u: any, seriesIdx: any, size: number): number => size / 4,
stroke: (
u: {
series: {
[x: string]: { points: { stroke: (arg0: any, arg1: any) => any } };
};
},
seriesIdx: string | number,
): string => `${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
fill: (): string => '#fff',
},
},
series: [
{
label: 'Time',
@@ -195,7 +158,6 @@ function AnomalyAlertEvaluationView({
width: 2,
show: true,
paths: _spline,
spanGaps: true,
},
{
label: `Predicted Value`,
@@ -204,29 +166,18 @@ function AnomalyAlertEvaluationView({
dash: [2, 2],
show: true,
paths: _spline,
spanGaps: true,
},
{
label: `Upper Band`,
stroke: 'transparent',
show: true,
show: false,
paths: _spline,
spanGaps: true,
points: {
show: false,
size: 1,
},
},
{
label: `Lower Band`,
stroke: 'transparent',
show: true,
show: false,
paths: _spline,
spanGaps: true,
points: {
show: false,
size: 1,
},
},
]
: allSeries.map((seriesKey) => ({
@@ -235,13 +186,11 @@ function AnomalyAlertEvaluationView({
width: 2,
show: true,
paths: _spline,
spanGaps: true,
}))),
],
scales: {
x: {
time: true,
spanGaps: true,
},
y: {
...getYAxisScaleForAnomalyDetection({
@@ -255,30 +204,12 @@ function AnomalyAlertEvaluationView({
grid: {
show: true,
},
legend: {
show: true,
},
axes: getAxes(isDarkMode, yAxisUnit),
};
const handleSearch = (searchText: string): void => {
if (!searchText || searchText.length === 0) {
setFilteredSeriesKeys(allSeries);
return;
}
const filteredSeries = allSeries.filter((series) =>
series.toLowerCase().includes(searchText.toLowerCase()),
);
setFilteredSeriesKeys(filteredSeries);
};
const handleSearchValueChange = useDebouncedFn((event): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const value = event?.target?.value || '';
handleSearch(value);
}, 300);
return (
<div className="anomaly-alert-evaluation-view">
<div
@@ -306,51 +237,37 @@ function AnomalyAlertEvaluationView({
<div className="anomaly-alert-evaluation-view-series-selection">
{allSeries.length > 1 && (
<div className="anomaly-alert-evaluation-view-series-list">
<Search
className="anomaly-alert-evaluation-view-series-list-search"
placeholder="Search a series"
allowClear
onChange={handleSearchValueChange}
/>
<Typography.Title
level={5}
className="anomaly-alert-evaluation-view-series-list-title"
>
Select a series
</Typography.Title>
<div className="anomaly-alert-evaluation-view-series-list-items">
{filteredSeriesKeys.length > 0 && (
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
type="checkbox"
name="series"
value="all"
checked={selectedSeries === null}
onChange={(): void => handleSeriesChange(null)}
>
Show All
</Checkbox>
{allSeries.map((seriesKey) => (
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
key={seriesKey}
type="checkbox"
name="series"
value="all"
checked={selectedSeries === null}
onChange={(): void => handleSeriesChange(null)}
value={seriesKey}
checked={selectedSeries === seriesKey}
onChange={(): void => handleSeriesChange(seriesKey)}
>
Show All
{seriesKey}
</Checkbox>
)}
{filteredSeriesKeys.map((seriesKey) => (
<div key={seriesKey}>
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
key={seriesKey}
type="checkbox"
name="series"
value={seriesKey}
checked={selectedSeries === seriesKey}
onChange={(): void => handleSeriesChange(seriesKey)}
>
<div
className="anomaly-alert-evaluation-view-series-list-item-color"
style={{ backgroundColor: seriesData[seriesKey].color }}
/>
{seriesKey}
</Checkbox>
</div>
))}
{filteredSeriesKeys.length === 0 && (
<Typography>No series found</Typography>
)}
</div>
</div>
)}

View File

@@ -1,148 +0,0 @@
import { themeColors } from 'constants/theme';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
const tooltipPlugin = (
isDarkMode: boolean,
): { hooks: { init: (u: any) => void } } => {
let tooltip: HTMLDivElement;
const tooltipLeftOffset = 10;
const tooltipTopOffset = 10;
let isMouseOverPlot = false;
function formatValue(value: string | number | Date): string | number | Date {
if (typeof value === 'string' && !Number.isNaN(parseFloat(value))) {
return parseFloat(value).toFixed(3);
}
if (typeof value === 'number') {
return value.toFixed(3);
}
if (value instanceof Date) {
return value.toLocaleString();
}
if (value == null) {
return 'N/A';
}
return String(value);
}
function updateTooltip(u: any, left: number, top: number): void {
const idx = u.posToIdx(left);
const xVal = u.data[0][idx];
if (xVal == null) {
tooltip.style.display = 'none';
return;
}
const xDate = new Date(xVal * 1000);
const formattedXDate = formatValue(xDate);
let tooltipContent = `<div class="uplot-tooltip-title">Time: ${formattedXDate}</div>`;
let mainValue;
let upperBand;
let lowerBand;
let color = null;
// Loop through all series (excluding the x-axis series)
for (let i = 1; i < u.series.length; i++) {
const series = u.series[i];
const yVal = u.data[i][idx];
const formattedYVal = formatValue(yVal);
color = generateColor(
series.label,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
// Create the round marker for the series
const marker = `<span class="uplot-tooltip-marker" style="background-color: ${color};"></span>`;
if (series.label.toLowerCase().includes('upper band')) {
upperBand = formattedYVal;
} else if (series.label.toLowerCase().includes('lower band')) {
lowerBand = formattedYVal;
} else if (series.label.toLowerCase().includes('main series')) {
mainValue = formattedYVal;
} else {
tooltipContent += `
<div class="uplot-tooltip-series">
${marker}
<span class="uplot-tooltip-series-name">${series.label}:</span>
<span class="uplot-tooltip-series-value">${formattedYVal}</span>
</div>`;
}
}
// Add main value, upper band, and lower band to the tooltip
if (mainValue !== undefined) {
const marker = `<span class="uplot-tooltip-marker"></span>`;
tooltipContent += `
<div class="uplot-tooltip-series">
${marker}
<span class="uplot-tooltip-series-name">Main Series:</span>
<span class="uplot-tooltip-series-value">${mainValue}</span>
</div>`;
}
if (upperBand !== undefined) {
const marker = `<span class="uplot-tooltip-marker"></span>`;
tooltipContent += `
<div class="uplot-tooltip-series">
${marker}
<span class="uplot-tooltip-series-name">Upper Band:</span>
<span class="uplot-tooltip-series-value">${upperBand}</span>
</div>`;
}
if (lowerBand !== undefined) {
const marker = `<span class="uplot-tooltip-marker"></span>`;
tooltipContent += `
<div class="uplot-tooltip-series">
${marker}
<span class="uplot-tooltip-series-name">Lower Band:</span>
<span class="uplot-tooltip-series-value">${lowerBand}</span>
</div>`;
}
tooltip.innerHTML = tooltipContent;
tooltip.style.display = 'block';
tooltip.style.left = `${left + tooltipLeftOffset}px`;
tooltip.style.top = `${top + tooltipTopOffset}px`;
}
function init(u: any): void {
tooltip = document.createElement('div');
tooltip.className = 'uplot-tooltip';
tooltip.style.display = 'none';
u.over.appendChild(tooltip);
// Add event listeners
u.over.addEventListener('mouseenter', () => {
isMouseOverPlot = true;
});
u.over.addEventListener('mouseleave', () => {
isMouseOverPlot = false;
tooltip.style.display = 'none';
});
u.over.addEventListener('mousemove', (e: MouseEvent) => {
if (isMouseOverPlot) {
const rect = u.over.getBoundingClientRect();
const left = e.clientX - rect.left;
const top = e.clientY - rect.top;
updateTooltip(u, left, top);
}
});
}
return {
hooks: {
init,
},
};
};
export default tooltipPlugin;

View File

@@ -211,13 +211,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
}, [licenseData, isFetching]);
useEffect(() => {
// after logging out hide the trial expiry banner
if (!isLoggedIn) {
setShowTrialExpiryBanner(false);
}
}, [isLoggedIn]);
const handleUpgrade = (): void => {
if (role === 'ADMIN') {
history.push(ROUTES.BILLING);

View File

@@ -4,7 +4,6 @@ import {
initialQueryPromQLData,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
AlertDef,
@@ -59,49 +58,6 @@ export const alertDefaults: AlertDef = {
evalWindow: defaultEvalWindow,
};
export const anamolyAlertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
condition: {
compositeQuery: {
builderQueries: {
A: {
...initialQueryBuilderFormValuesMap.metrics,
functions: [
{
name: 'anomaly',
args: [],
namedArgs: { z_score_threshold: 3 },
},
],
},
},
promQueries: { A: initialQueryPromQLData },
chQueries: {
A: {
name: 'A',
query: ``,
legend: '',
disabled: false,
},
},
queryType: EQueryType.QUERY_BUILDER,
panelType: PANEL_TYPES.TIME_SERIES,
unit: undefined,
},
op: defaultCompareOp,
matchType: defaultMatchType,
algorithm: defaultAlgorithm,
seasonality: defaultSeasonality,
},
labels: {
severity: 'warning',
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
};
export const logAlertDefaults: AlertDef = {
alertType: AlertTypes.LOGS_BASED_ALERT,
condition: {
@@ -193,7 +149,7 @@ export const exceptionAlertDefaults: AlertDef = {
};
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
[AlertTypes.ANOMALY_BASED_ALERT]: anamolyAlertDefaults,
[AlertTypes.ANOMALY_BASED_ALERT]: alertDefaults,
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,

View File

@@ -13,7 +13,6 @@ import { AlertDef } from 'types/api/alerts/def';
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
import {
alertDefaults,
anamolyAlertDefaults,
exceptionAlertDefaults,
logAlertDefaults,
traceAlertDefaults,
@@ -25,12 +24,8 @@ function CreateRules(): JSX.Element {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const alertTypeFromURL = queryParams.get(QueryParams.ruleType);
const version = queryParams.get('version');
const alertTypeFromParams =
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
? AlertTypes.ANOMALY_BASED_ALERT
: queryParams.get(QueryParams.alertType);
const alertTypeFromParams = queryParams.get(QueryParams.alertType);
const compositeQuery = useGetCompositeQueryParam();
function getAlertTypeFromDataSource(): AlertTypes | null {
@@ -63,7 +58,7 @@ function CreateRules(): JSX.Element {
break;
case AlertTypes.ANOMALY_BASED_ALERT:
setInitValues({
...anamolyAlertDefaults,
...alertDefaults,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
});
@@ -83,10 +78,7 @@ function CreateRules(): JSX.Element {
: typ,
);
if (
typ === AlertTypes.ANOMALY_BASED_ALERT ||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
) {
if (typ === AlertTypes.ANOMALY_BASED_ALERT) {
queryParams.set(
QueryParams.ruleType,
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,

View File

@@ -18,7 +18,7 @@
display: inline-flex;
align-items: center;
gap: 12px;
padding: 10px 10px;
padding: 10px 12px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6);
@@ -33,7 +33,6 @@
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
cursor: pointer;
box-shadow: none;
}
.hidden {

View File

@@ -45,6 +45,7 @@ import {
PanelBottomClose,
Plus,
X,
XCircle,
} from 'lucide-react';
import {
CSSProperties,
@@ -514,11 +515,7 @@ function ExplorerOptions({
return (
<div className="explorer-options-container">
{
// if a viewName is selected and the explorer options are not hidden then
// always show the clear option
}
{!isExplorerOptionHidden && viewName && (
{isQueryUpdated && !isExplorerOptionHidden && (
<div
className={cx(
isEditDeleteSupported ? '' : 'hide-update',
@@ -532,25 +529,18 @@ function ExplorerOptions({
icon={<X size={14} />}
/>
</Tooltip>
{
// only show the update view option when the query is updated
}
{isQueryUpdated && (
<>
<Divider
type="vertical"
className={isEditDeleteSupported ? '' : 'hidden'}
/>
<Tooltip title="Update this view" placement="top">
<Button
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={14} />}
/>
</Tooltip>
</>
)}
<Divider
type="vertical"
className={isEditDeleteSupported ? '' : 'hidden'}
/>
<Tooltip title="Update this view" placement="top">
<Button
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={14} />}
/>
</Tooltip>
</div>
)}
{!isExplorerOptionHidden && (
@@ -574,7 +564,10 @@ function ExplorerOptions({
}}
dropdownStyle={dropdownStyle}
className="views-dropdown"
allowClear={false}
allowClear={{
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
}}
onClear={handleClearSelect}
ref={ref}
>
{viewsData?.data?.data?.map((view) => {
@@ -669,8 +662,8 @@ function ExplorerOptions({
</div>
</div>
)}
<ExplorerOptionsHideArea
viewName={viewName}
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
sourcepage={sourcepage}
@@ -679,6 +672,7 @@ function ExplorerOptions({
onUpdateQueryHandler={onUpdateQueryHandler}
isEditDeleteSupported={isEditDeleteSupported}
/>
<Modal
className="save-view-modal"
title={<span className="title">Save this view</span>}
@@ -711,6 +705,7 @@ function ExplorerOptions({
/>
</div>
</Modal>
<Modal
footer={null}
onOk={onCancel(false)}

View File

@@ -10,7 +10,6 @@ import { DataSource } from 'types/common/queryBuilder';
import { setExplorerToolBarVisibility } from './utils';
interface DroppableAreaProps {
viewName: string;
isQueryUpdated: boolean;
isExplorerOptionHidden?: boolean;
sourcepage: DataSource;
@@ -21,7 +20,6 @@ interface DroppableAreaProps {
}
function ExplorerOptionsHideArea({
viewName,
isQueryUpdated,
isExplorerOptionHidden,
sourcepage,
@@ -41,7 +39,7 @@ function ExplorerOptionsHideArea({
<div className="explorer-option-droppable-container">
{isExplorerOptionHidden && (
<>
{viewName && (
{isQueryUpdated && (
<div className="explorer-actions-btn">
<Tooltip title="Clear this view">
<Button
@@ -51,7 +49,7 @@ function ExplorerOptionsHideArea({
icon={<X size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
{isEditDeleteSupported && isQueryUpdated && (
{isEditDeleteSupported && (
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}

View File

@@ -160,15 +160,6 @@ function RuleOptions({
});
};
const onChangeDeviation = (value: number): void => {
const target = value || alertDef.condition.target || 3;
setAlertDef({
...alertDef,
condition: { ...alertDef.condition, target: Number(target) },
});
};
const renderEvalWindows = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
@@ -212,28 +203,6 @@ function RuleOptions({
</InlineSelect>
);
const renderDeviationOpts = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={3}
style={{ minWidth: '120px' }}
value={alertDef.condition.target}
onChange={(value: number | unknown): void => {
if (typeof value === 'number') {
onChangeDeviation(value);
}
}}
>
<Select.Option value={1}>1</Select.Option>
<Select.Option value={2}>2</Select.Option>
<Select.Option value={3}>3</Select.Option>
<Select.Option value={4}>4</Select.Option>
<Select.Option value={5}>5</Select.Option>
<Select.Option value={6}>6</Select.Option>
<Select.Option value={7}>7</Select.Option>
</InlineSelect>
);
const renderSeasonality = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
@@ -268,6 +237,39 @@ function RuleOptions({
</Form.Item>
);
const renderAnomalyRuleOpts = (
onChange: InputNumberProps['onChange'],
): JSX.Element => (
<Form.Item>
<Typography.Text className="rule-definition">
{t('text_condition1_anomaly')}
<InlineSelect
getPopupContainer={popupContainer}
allowClear
showSearch
options={queryOptions}
placeholder={t('selected_query_placeholder')}
value={alertDef.condition.selectedQueryName}
onChange={onChangeSelectedQueryName}
/>
{t('text_condition3')} {renderEvalWindows()}
<Typography.Text>is</Typography.Text>
<InputNumber
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
<Typography.Text>deviations</Typography.Text>
{renderCompareOps()}
<Typography.Text>the predicted data</Typography.Text>
{renderMatchOpts()}
using the {renderAlgorithms()} algorithm with {renderSeasonality()}{' '}
seasonality
</Typography.Text>
</Form.Item>
);
const renderPromRuleOptions = (): JSX.Element => (
<Form.Item>
<Typography.Text>
@@ -318,32 +320,6 @@ function RuleOptions({
});
};
const renderAnomalyRuleOpts = (): JSX.Element => (
<Form.Item>
<Typography.Text className="rule-definition">
{t('text_condition1_anomaly')}
<InlineSelect
getPopupContainer={popupContainer}
allowClear
showSearch
options={queryOptions}
placeholder={t('selected_query_placeholder')}
value={alertDef.condition.selectedQueryName}
onChange={onChangeSelectedQueryName}
/>
{t('text_condition3')} {renderEvalWindows()}
<Typography.Text>is</Typography.Text>
{renderDeviationOpts()}
<Typography.Text>deviations</Typography.Text>
{renderCompareOps()}
<Typography.Text>the predicted data</Typography.Text>
{renderMatchOpts()}
using the {renderAlgorithms()} algorithm with {renderSeasonality()}{' '}
seasonality
</Typography.Text>
</Form.Item>
);
const renderFrequency = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
@@ -378,7 +354,7 @@ function RuleOptions({
{queryCategory === EQueryType.PROM && renderPromRuleOptions()}
{queryCategory !== EQueryType.PROM &&
ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
<>{renderAnomalyRuleOpts()}</>
<>{renderAnomalyRuleOpts(onChange)}</>
)}
{queryCategory !== EQueryType.PROM &&

View File

@@ -39,7 +39,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
@@ -89,8 +88,6 @@ function FormAlertRules({
>((state) => state.globalTime);
const urlQuery = useUrlQuery();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
// In case of alert the panel types should always be "Graph" only
const panelType = PANEL_TYPES.TIME_SERIES;
@@ -123,7 +120,9 @@ function FormAlertRules({
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
const [detectionMethod, setDetectionMethod] = useState<string>(
AlertDetectionTypes.THRESHOLD_ALERT,
);
useEffect(() => {
if (!isEqual(currentQuery.unit, yAxisUnit)) {
@@ -155,24 +154,11 @@ function FormAlertRules({
useShareBuilderUrl(sq);
const handleDetectionMethodChange = (value: string): void => {
setAlertDef((def) => ({
...def,
ruleType: value,
}));
logEvent(`Alert: Detection method changed`, {
detectionMethod: value,
});
setDetectionMethod(value);
};
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
const anomalyFunction = {
name: 'anomaly',
args: [],
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
namedArgs: { z_score_threshold: 9 },
};
const functions = data.functions || [];
@@ -180,19 +166,6 @@ function FormAlertRules({
// Add anomaly if not already present
if (!functions.some((func) => func.name === 'anomaly')) {
functions.push(anomalyFunction);
} else {
const anomalyFuncIndex = functions.findIndex(
(func) => func.name === 'anomaly',
);
if (anomalyFuncIndex !== -1) {
const anomalyFunc = {
...functions[anomalyFuncIndex],
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
};
functions.splice(anomalyFuncIndex, 1);
functions.push(anomalyFunc);
}
}
} else {
// Remove anomaly if present
@@ -205,20 +178,6 @@ function FormAlertRules({
return functions;
};
useEffect(() => {
const ruleType =
detectionMethod === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
? AlertDetectionTypes.ANOMALY_DETECTION_ALERT
: AlertDetectionTypes.THRESHOLD_ALERT;
queryParams.set(QueryParams.ruleType, ruleType);
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
history.replace(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [detectionMethod]);
const updateFunctionsBasedOnAlertType = (): void => {
for (let index = 0; index < currentQuery.builder.queryData.length; index++) {
const queryData = currentQuery.builder.queryData[index];
@@ -232,11 +191,7 @@ function FormAlertRules({
useEffect(() => {
updateFunctionsBasedOnAlertType();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
detectionMethod,
alertDef.condition.target,
currentQuery.builder.queryData.length,
]);
}, [detectionMethod, alertDef, currentQuery.builder.queryData.length]);
useEffect(() => {
const broadcastToSpecificChannels =
@@ -260,8 +215,7 @@ function FormAlertRules({
});
setDetectionMethod(ruleType);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValue, isNewRule]);
}, [initialValue, isNewRule, alertTypeFromURL]);
useEffect(() => {
// Set selectedQueryName based on the length of queryOptions
@@ -381,11 +335,7 @@ function FormAlertRules({
return false;
}
if (
alertDef.ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT &&
alertDef.condition?.target !== 0 &&
!alertDef.condition?.target
) {
if (alertDef.condition?.target !== 0 && !alertDef.condition?.target) {
notifications.error({
message: 'Error',
description: t('target_missing'),
@@ -543,7 +493,6 @@ function FormAlertRules({
queryType: currentQuery.queryType,
alertId: postableAlert?.id,
alertName: postableAlert?.alert,
ruleType: postableAlert?.ruleType,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
@@ -628,7 +577,6 @@ function FormAlertRules({
queryType: currentQuery.queryType,
status: statusResponse.status,
statusMessage: statusResponse.message,
ruleType: postableAlert.ruleType,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [t, isFormValid, memoizedPreparePostData, notifications]);
@@ -724,6 +672,15 @@ function FormAlertRules({
},
];
const handleDetectionMethodChange = (value: any): void => {
setAlertDef((def) => ({
...def,
ruleType: value,
}));
setDetectionMethod(value);
};
const isAnomalyDetectionEnabled =
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
@@ -788,7 +745,7 @@ function FormAlertRules({
<Tabs2
key={detectionMethod}
tabs={tabs}
initialSelectedTab={detectionMethod || ''}
initialSelectedTab={detectionMethod}
onSelectTab={handleDetectionMethodChange}
/>

View File

@@ -97,19 +97,13 @@ function GridTableComponent({
const newColumnData = columns.map((e) => ({
...e,
render: (text: string, ...rest: any): ReactNode => {
let textForThreshold = text;
if (columnUnits && columnUnits?.[e.title as string]) {
textForThreshold = rest[0][`${e.title}_without_unit`];
}
const isNumber = !Number.isNaN(Number(textForThreshold));
render: (text: string): ReactNode => {
const isNumber = !Number.isNaN(Number(text));
if (thresholds && isNumber) {
const { hasMultipleMatches, threshold } = findMatchingThreshold(
thresholds,
e.title as string,
Number(textForThreshold),
columnUnits?.[e.title as string],
Number(text),
);
const idx = thresholds.findIndex(

View File

@@ -1,6 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { ColumnsType, ColumnType } from 'antd/es/table';
import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
@@ -31,39 +30,10 @@ function evaluateCondition(
}
}
/**
* Evaluates whether a given value meets a specified threshold condition.
* It first converts the value to the appropriate unit if a threshold unit is provided,
* and then checks the condition using the specified operator.
*
* @param value - The value to be evaluated.
* @param thresholdValue - The threshold value to compare against.
* @param thresholdOperator - The operator used for comparison (e.g., '>', '<', '==').
* @param thresholdUnit - The unit to which the value should be converted.
* @param columnUnit - The current unit of the value.
* @returns A boolean indicating whether the value meets the threshold condition.
*/
function evaluateThresholdWithConvertedValue(
value: number,
thresholdValue: number,
thresholdOperator?: string,
thresholdUnit?: string,
columnUnit?: string,
): boolean {
const convertedValue = convertUnit(value, columnUnit, thresholdUnit);
if (convertedValue) {
return evaluateCondition(thresholdOperator, convertedValue, thresholdValue);
}
return evaluateCondition(thresholdOperator, value, thresholdValue);
}
export function findMatchingThreshold(
thresholds: ThresholdProps[],
label: string,
value: number,
columnUnit?: string,
): {
threshold: ThresholdProps;
hasMultipleMatches: boolean;
@@ -75,12 +45,10 @@ export function findMatchingThreshold(
if (
threshold.thresholdValue !== undefined &&
threshold.thresholdTableOptions === label &&
evaluateThresholdWithConvertedValue(
value,
threshold?.thresholdValue,
evaluateCondition(
threshold.thresholdOperator,
threshold.thresholdUnit,
columnUnit,
value,
threshold.thresholdValue,
)
) {
matchingThresholds.push(threshold);

View File

@@ -5,6 +5,7 @@ import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';
import DropDown from 'components/DropDown/DropDown';
import { listAlertMessage } from 'components/LaunchChatSupport/util';
import {
DynamicColumnsKey,
TableDataSource,
@@ -396,6 +397,15 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
dynamicColumns={dynamicColumns}
onChange={handleChange}
pagination={paginationConfig}
facingIssueBtn={{
attributes: {
screen: 'Alert list page',
},
eventName: 'Alert: Facing Issues in alert',
buttonText: 'Facing issues with alerts?',
message: listAlertMessage,
onHoverText: 'Click here to get help with alerts',
}}
/>
</>
);

View File

@@ -5,17 +5,6 @@
justify-content: center;
width: 100%;
// overridding the request integration style to fix the spacing for dashboard list
.request-entity-container {
margin-bottom: 16px !important;
margin-top: 0 !important;
}
.integrations-content {
max-width: 100% !important;
width: 100% !important;
}
.dashboards-list-view-content {
width: calc(100% - 30px);
max-width: 836px;

View File

@@ -25,6 +25,8 @@ import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardListMessage } from 'components/LaunchChatSupport/util';
import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes';
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
@@ -77,7 +79,6 @@ import { isCloudUser } from 'utils/app';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
import { RequestDashboardBtn } from './RequestDashboardBtn';
import { DeleteButton } from './TableComponents/DeleteButton';
import {
DashboardDynamicColumns,
@@ -692,14 +693,17 @@ function DashboardsList(): JSX.Element {
<Typography.Text className="subtitle">
Create and manage dashboards for your workspace.
</Typography.Text>
<LaunchChatSupport
attributes={{
screen: 'Dashboard list page',
}}
eventName="Dashboard: Facing Issues in dashboard"
message={dashboardListMessage}
buttonText="Need help with dashboards?"
onHoverText="Click here to get help with dashboards"
intercomMessageDisabled
/>
</Flex>
{isCloudUser() && (
<div className="integrations-container">
<div className="integrations-content">
<RequestDashboardBtn />
</div>
</div>
)}
</div>
{isDashboardListLoading ||

View File

@@ -82,12 +82,6 @@ function ImportJSON({
const dashboardData = JSON.parse(editorValue) as DashboardData;
// Add validation for uuid
if (dashboardData.uuid !== undefined && dashboardData.uuid.trim() === '') {
// silently remove uuid if it is empty
delete dashboardData.uuid;
}
if (dashboardData?.layout) {
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
} else {
@@ -129,14 +123,11 @@ function ImportJSON({
});
}
setDashboardCreating(false);
} catch (error) {
} catch {
setDashboardCreating(false);
setIsFeatureAlert(false);
setIsCreateDashboardError(true);
notifications.error({
message: error instanceof Error ? error.message : t('error_loading_json'),
});
}
};

View File

@@ -1,95 +0,0 @@
import '../../pages/Integrations/Integrations.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Input, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useNotifications } from 'hooks/useNotifications';
import { Check } from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
export function RequestDashboardBtn(): JSX.Element {
const [
isSubmittingRequestForDashboard,
setIsSubmittingRequestForDashboard,
] = useState(false);
const [requestedDashboardName, setRequestedDashboardName] = useState('');
const { notifications } = useNotifications();
const { t } = useTranslation(['common']);
const handleRequestDashboardSubmit = async (): Promise<void> => {
try {
setIsSubmittingRequestForDashboard(true);
const response = await logEvent('Dashboard Requested', {
screen: 'Dashboard list page',
dashboard: requestedDashboardName,
});
if (response.statusCode === 200) {
notifications.success({
message: 'Dashboard Request Submitted',
});
setIsSubmittingRequestForDashboard(false);
} else {
notifications.error({
message:
response.error ||
t('something_went_wrong', {
ns: 'common',
}),
});
setIsSubmittingRequestForDashboard(false);
}
} catch (error) {
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
setIsSubmittingRequestForDashboard(false);
}
};
return (
<div className="request-entity-container">
<Typography.Text>
Can&apos;t find the dashboard you need? Request a new Dashboard.
</Typography.Text>
<div className="form-section">
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="Enter dashboard name..."
style={{ width: 300, marginBottom: 0 }}
value={requestedDashboardName}
onChange={(e): void => setRequestedDashboardName(e.target.value)}
/>
<Button
className="periscope-btn primary"
icon={
isSubmittingRequestForDashboard ? (
<LoadingOutlined />
) : (
<Check size={12} />
)
}
type="primary"
onClick={handleRequestDashboardSubmit}
disabled={
isSubmittingRequestForDashboard ||
!requestedDashboardName ||
requestedDashboardName?.trim().length === 0
}
>
Submit
</Button>
</Space.Compact>
</div>
</div>
);
}

View File

@@ -2,25 +2,3 @@
cursor: pointer;
position: relative;
}
.table-row-backdrop {
&.INFO {
background-color: var(--bg-robin-500) 10;
}
&.WARNING,
&.WARN {
background-color: var(--bg-amber-500) 10;
}
&.ERROR {
background-color: var(--bg-cherry-500) 10;
}
&.TRACE {
background-color: var(--bg-forest-400) 10;
}
&.DEBUG {
background-color: var(--bg-aqua-500) 10;
}
&.FATAL {
background-color: var(--bg-sakura-500) 10;
}
}

View File

@@ -1,6 +1,5 @@
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { useTableView } from 'components/Logs/TableView/useTableView';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -22,11 +21,6 @@ import { TableHeaderCellStyled, TableRowStyled } from './styles';
import TableRow from './TableRow';
import { InfinityTableProps } from './types';
interface CustomTableRowProps {
activeContextLogId: string;
activeLogId: string;
}
// eslint-disable-next-line react/function-component-definition
const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
children,
@@ -37,17 +31,10 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
const isDarkMode = useIsDarkMode();
const logType = getLogIndicatorType(props.item);
return (
<TableRowStyled
$isDarkMode={isDarkMode}
$isActiveLog={
isHighlighted ||
(context as CustomTableRowProps).activeContextLogId === props.item.id ||
(context as CustomTableRowProps).activeLogId === props.item.id
}
$logType={logType}
$isActiveLog={isHighlighted}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
@@ -79,6 +66,8 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
...tableViewProps,
onClickExpand: onSetActiveLog,
onOpenLogsContext: handleSetActiveContextLog,
activeLog,
activeContextLog,
});
const { draggedColumns, onDragColumns } = useDragColumns<
@@ -164,14 +153,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
// TODO: fix it in the future
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
TableRow: (props): any =>
CustomTableRow({
...props,
context: {
activeContextLogId: activeContextLog?.id,
activeLogId: activeLog?.id,
},
} as any),
TableRow: CustomTableRow,
}}
itemContent={itemContent}
fixedHeaderContent={tableHeader}

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-nested-ternary */
import { Color } from '@signozhq/design-tokens';
import { themeColors } from 'constants/theme';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
@@ -36,12 +37,13 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
export const TableRowStyled = styled.tr<{
$isActiveLog: boolean;
$isDarkMode: boolean;
$logType: string;
}>`
td {
${({ $isActiveLog, $isDarkMode, $logType }): string =>
${({ $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
: ''};
}

View File

@@ -12,6 +12,8 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardHelpMessage } from 'components/LaunchChatSupport/util';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
@@ -45,11 +47,7 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import {
Dashboard,
DashboardData,
IDashboardVariable,
} from 'types/api/dashboard/getAll';
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -65,30 +63,6 @@ interface DashboardDescriptionProps {
handle: FullScreenHandle;
}
function sanitizeDashboardData(
selectedData: DashboardData,
): Omit<DashboardData, 'uuid'> {
if (!selectedData?.variables) {
const { uuid, ...rest } = selectedData;
return rest;
}
const updatedVariables = Object.entries(selectedData.variables).reduce(
(acc, [key, value]) => {
const { selectedValue, ...rest } = value;
acc[key] = rest;
return acc;
},
{} as Record<string, IDashboardVariable>,
);
const { uuid, ...restData } = selectedData;
return {
...restData,
variables: updatedVariables,
};
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { handle } = props;
@@ -354,6 +328,18 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
{isDashboardLocked && <LockKeyhole size={14} />}
</div>
<div className="right-section">
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: updatedTitle,
screen: 'Dashboard Details',
}}
eventName="Dashboard: Facing Issues in dashboard"
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
buttonText="Need help with this dashboard?"
onHoverText="Click here to get help with dashboard"
intercomMessageDisabled
/>
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
<Popover
open={isDashboardSettingsOpen}
@@ -421,10 +407,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
type="text"
icon={<FileJson size={14} />}
onClick={(): void => {
downloadObjectAsJson(
sanitizeDashboardData(selectedData),
selectedData.title,
);
downloadObjectAsJson(selectedData, selectedData.title);
setIsDashbordSettingsOpen(false);
}}
>
@@ -434,9 +417,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
type="text"
icon={<ClipboardCopy size={14} />}
onClick={(): void => {
setCopy(
JSON.stringify(sanitizeDashboardData(selectedData), null, 2),
);
setCopy(JSON.stringify(selectedData, null, 2));
setIsDashbordSettingsOpen(false);
}}
>

View File

@@ -257,7 +257,8 @@ function VariableItem({
if (variableData.name) {
if (
value === ALL_SELECT_VALUE ||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
(Array.isArray(value) && value.length === 0)
) {
onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else {
@@ -323,6 +324,10 @@ function VariableItem({
Array.isArray(selectedValueStringified) &&
selectedValueStringified.includes(option.toString())
) {
if (newSelectedValue.length === 0) {
handleChange(ALL_SELECT_VALUE);
return;
}
if (newSelectedValue.length === 1) {
handleChange(newSelectedValue[0].toString());
return;

View File

@@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
@@ -234,6 +235,21 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: selectedDashboard?.data.title,
screen: 'Dashboard widget',
panelType: selectedGraph,
widgetId: query.id,
queryType: currentQuery.queryType,
}}
eventName="Dashboard: Facing Issues in dashboard"
buttonText="Need help with this chart?"
// message={chartHelpMessage(selectedDashboard, graphType)}
onHoverText="Click here to get help with this dashboard widget"
intercomMessageDisabled
/>
<TextToolTip
text="This will temporarily save the current query and graph state. This will persist across tab change"
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"

View File

@@ -297,16 +297,6 @@
box-shadow: none;
}
}
.invalid-unit {
color: var(--bg-vanilla-400);
font-family: 'Giest Mono';
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.48px;
}
}
.threshold-card-container:hover {

View File

@@ -3,18 +3,17 @@ import './Threshold.styles.scss';
import { Button, Input, InputNumber, Select, Space, Typography } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { unitOptions } from 'container/NewWidget/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Check, Pencil, Trash2, X } from 'lucide-react';
import { useMemo, useRef, useState } from 'react';
import { useRef, useState } from 'react';
import { useDrag, useDrop, XYCoord } from 'react-dnd';
import {
operatorOptions,
panelTypeVsDragAndDrop,
showAsOptions,
unitOptions,
} from '../constants';
import { convertUnit } from '../dataFormatCategories';
import ColorSelector from './ColorSelector';
import CustomColor from './CustomColor';
import ShowCaseValue from './ShowCaseValue';
@@ -41,7 +40,6 @@ function Threshold({
thresholdLabel = '',
tableOptions,
thresholdTableOptions = '',
columnUnits,
}: ThresholdProps): JSX.Element {
const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled);
const [operator, setOperator] = useState<string | number>(
@@ -194,13 +192,6 @@ function Threshold({
const allowDragAndDrop = panelTypeVsDragAndDrop[selectedGraph];
const isInvalidUnitComparison = useMemo(
() =>
unit !== 'none' &&
convertUnit(value, unit, columnUnits?.[tableSelectedOption]) === null,
[unit, value, columnUnits, tableSelectedOption],
);
return (
<div
ref={allowDragAndDrop ? ref : null}
@@ -312,7 +303,7 @@ function Threshold({
{isEditMode ? (
<Select
defaultValue={unit}
options={unitOptions(columnUnits?.[tableSelectedOption] || '')}
options={unitOptions}
onChange={handleUnitChange}
showSearch
className="unit-selection"
@@ -348,12 +339,6 @@ function Threshold({
</>
)}
</div>
{isInvalidUnitComparison && (
<Typography.Text className="invalid-unit">
Threshold unit ({unit}) is not valid in comparison with the column unit (
{columnUnits?.[tableSelectedOption] || 'none'})
</Typography.Text>
)}
{isEditMode && (
<div className="threshold-action-button">
<Button

View File

@@ -3,12 +3,14 @@
import './ThresholdSelector.styles.scss';
import { Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ColumnsType } from 'antd/es/table';
import { Events } from 'constants/events';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { Antenna, Plus } from 'lucide-react';
import { useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { EQueryType } from 'types/common/dashboard';
import { eventEmitter } from 'utils/getEventEmitter';
import { v4 as uuid } from 'uuid';
import Threshold from './Threshold';
@@ -19,23 +21,22 @@ function ThresholdSelector({
setThresholds,
yAxisUnit,
selectedGraph,
columnUnits,
}: ThresholdSelectorProps): JSX.Element {
const { currentQuery } = useQueryBuilder();
function getAggregateColumnsNamesAndLabels(): string[] {
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
const queries = currentQuery.builder.queryData.map((q) => q.queryName);
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
return [...queries, ...formulas];
}
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
return currentQuery.clickhouse_sql.map((q) => q.name);
}
return currentQuery.promql.map((q) => q.name);
}
const aggregationQueries = getAggregateColumnsNamesAndLabels();
const [tableOptions, setTableOptions] = useState<
Array<{ value: string; label: string }>
>([]);
useEffect(() => {
eventEmitter.on(
Events.TABLE_COLUMNS_DATA,
(data: { columns: ColumnsType<RowData>; dataSource: RowData[] }) => {
const newTableOptions = data.columns.map((e) => ({
value: e.title as string,
label: e.title as string,
}));
setTableOptions([...newTableOptions]);
},
);
}, []);
const moveThreshold = useCallback(
(dragIndex: number, hoverIndex: number) => {
@@ -65,7 +66,7 @@ function ThresholdSelector({
moveThreshold,
keyIndex: thresholds.length,
selectedGraph,
thresholdTableOptions: aggregationQueries[0] || '',
thresholdTableOptions: tableOptions[0]?.value || '',
},
...thresholds,
]);
@@ -104,12 +105,8 @@ function ThresholdSelector({
moveThreshold={moveThreshold}
selectedGraph={selectedGraph}
thresholdLabel={threshold.thresholdLabel}
tableOptions={aggregationQueries.map((query) => ({
value: query,
label: query,
}))}
tableOptions={tableOptions}
thresholdTableOptions={threshold.thresholdTableOptions}
columnUnits={columnUnits}
/>
))}
</div>

View File

@@ -1,6 +1,5 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Dispatch, ReactNode, SetStateAction } from 'react';
import { ColumnUnit } from 'types/api/dashboard/getAll';
export type ThresholdOperators = '>' | '<' | '>=' | '<=' | '=';
@@ -20,7 +19,6 @@ export type ThresholdProps = {
moveThreshold: (dragIndex: number, hoverIndex: number) => void;
selectedGraph: PANEL_TYPES;
tableOptions?: Array<{ value: string; label: string }>;
columnUnits?: ColumnUnit;
};
export type ShowCaseValueProps = {
@@ -38,5 +36,4 @@ export type ThresholdSelectorProps = {
thresholds: ThresholdProps[];
setThresholds: Dispatch<SetStateAction<ThresholdProps[]>>;
selectedGraph: PANEL_TYPES;
columnUnits: ColumnUnit;
};

View File

@@ -1,5 +1,8 @@
import { DefaultOptionType } from 'antd/es/select';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
import { getCategorySelectOptionByName } from './alertFomatCategories';
export const operatorOptions: DefaultOptionType[] = [
{ value: '>', label: '>' },
@@ -8,6 +11,11 @@ export const operatorOptions: DefaultOptionType[] = [
{ value: '<=', label: '<=' },
];
export const unitOptions = categoryToSupport.map((category) => ({
label: category,
options: getCategorySelectOptionByName(category),
}));
export const showAsOptions: DefaultOptionType[] = [
{ value: 'Text', label: 'Text' },
{ value: 'Background', label: 'Background' },

View File

@@ -438,168 +438,3 @@ export const dataTypeCategories: DataTypeCategories = [
export const flattenedCategories = flattenDeep(
dataTypeCategories.map((category) => category.formats),
);
type ConversionFactors = {
[key: string]: {
[key: string]: number | null;
};
};
// Object containing conversion factors for various categories and formats
const conversionFactors: ConversionFactors = {
[CategoryNames.Time]: {
[TimeFormats.Hertz]: 1,
[TimeFormats.Nanoseconds]: 1e-9,
[TimeFormats.Microseconds]: 1e-6,
[TimeFormats.Milliseconds]: 1e-3,
[TimeFormats.Seconds]: 1,
[TimeFormats.Minutes]: 60,
[TimeFormats.Hours]: 3600,
[TimeFormats.Days]: 86400,
[TimeFormats.DurationMs]: 1e-3,
[TimeFormats.DurationS]: 1,
[TimeFormats.DurationHms]: null, // Requires special handling
[TimeFormats.DurationDhms]: null, // Requires special handling
[TimeFormats.Timeticks]: null, // Requires special handling
[TimeFormats.ClockMs]: 1e-3,
[TimeFormats.ClockS]: 1,
},
[CategoryNames.Throughput]: {
[ThroughputFormats.CountsPerSec]: 1,
[ThroughputFormats.OpsPerSec]: 1,
[ThroughputFormats.RequestsPerSec]: 1,
[ThroughputFormats.ReadsPerSec]: 1,
[ThroughputFormats.WritesPerSec]: 1,
[ThroughputFormats.IOOpsPerSec]: 1,
[ThroughputFormats.CountsPerMin]: 1 / 60,
[ThroughputFormats.OpsPerMin]: 1 / 60,
[ThroughputFormats.ReadsPerMin]: 1 / 60,
[ThroughputFormats.WritesPerMin]: 1 / 60,
},
[CategoryNames.Data]: {
[DataFormats.BytesIEC]: 1,
[DataFormats.BytesSI]: 1,
[DataFormats.BitsIEC]: 0.125,
[DataFormats.BitsSI]: 0.125,
[DataFormats.KibiBytes]: 1024,
[DataFormats.KiloBytes]: 1000,
[DataFormats.MebiBytes]: 1048576,
[DataFormats.MegaBytes]: 1000000,
[DataFormats.GibiBytes]: 1073741824,
[DataFormats.GigaBytes]: 1000000000,
[DataFormats.TebiBytes]: 1099511627776,
[DataFormats.TeraBytes]: 1000000000000,
[DataFormats.PebiBytes]: 1125899906842624,
[DataFormats.PetaBytes]: 1000000000000000,
},
[CategoryNames.DataRate]: {
[DataRateFormats.PacketsPerSec]: null, // Cannot convert directly to other data rates
[DataRateFormats.BytesPerSecIEC]: 1,
[DataRateFormats.BytesPerSecSI]: 1,
[DataRateFormats.BitsPerSecIEC]: 0.125,
[DataRateFormats.BitsPerSecSI]: 0.125,
[DataRateFormats.KibiBytesPerSec]: 1024,
[DataRateFormats.KibiBitsPerSec]: 128,
[DataRateFormats.KiloBytesPerSec]: 1000,
[DataRateFormats.KiloBitsPerSec]: 125,
[DataRateFormats.MebiBytesPerSec]: 1048576,
[DataRateFormats.MebiBitsPerSec]: 131072,
[DataRateFormats.MegaBytesPerSec]: 1000000,
[DataRateFormats.MegaBitsPerSec]: 125000,
[DataRateFormats.GibiBytesPerSec]: 1073741824,
[DataRateFormats.GibiBitsPerSec]: 134217728,
[DataRateFormats.GigaBytesPerSec]: 1000000000,
[DataRateFormats.GigaBitsPerSec]: 125000000,
[DataRateFormats.TebiBytesPerSec]: 1099511627776,
[DataRateFormats.TebiBitsPerSec]: 137438953472,
[DataRateFormats.TeraBytesPerSec]: 1000000000000,
[DataRateFormats.TeraBitsPerSec]: 125000000000,
[DataRateFormats.PebiBytesPerSec]: 1125899906842624,
[DataRateFormats.PebiBitsPerSec]: 140737488355328,
[DataRateFormats.PetaBytesPerSec]: 1000000000000000,
[DataRateFormats.PetaBitsPerSec]: 125000000000000,
},
[CategoryNames.Miscellaneous]: {
[MiscellaneousFormats.None]: null,
[MiscellaneousFormats.String]: null,
[MiscellaneousFormats.Short]: null,
[MiscellaneousFormats.Percent]: 1,
[MiscellaneousFormats.PercentUnit]: 100,
[MiscellaneousFormats.Humidity]: 1,
[MiscellaneousFormats.Decibel]: null,
[MiscellaneousFormats.Hexadecimal0x]: null,
[MiscellaneousFormats.Hexadecimal]: null,
[MiscellaneousFormats.ScientificNotation]: null,
[MiscellaneousFormats.LocaleFormat]: null,
[MiscellaneousFormats.Pixels]: null,
},
[CategoryNames.Boolean]: {
[BooleanFormats.TRUE_FALSE]: null, // Not convertible
[BooleanFormats.YES_NO]: null, // Not convertible
[BooleanFormats.ON_OFF]: null, // Not convertible
},
};
// Function to get the conversion factor between two units in a specific category
function getConversionFactor(
fromUnit: string,
toUnit: string,
category: CategoryNames,
): number | null {
// Retrieves the conversion factors for the specified category
const categoryFactors = conversionFactors[category];
if (!categoryFactors) {
return null; // Returns null if the category does not exist
}
const fromFactor = categoryFactors[fromUnit];
const toFactor = categoryFactors[toUnit];
if (
fromFactor === undefined ||
toFactor === undefined ||
fromFactor === null ||
toFactor === null
) {
return null; // Returns null if either unit does not exist or is not convertible
}
return fromFactor / toFactor; // Returns the conversion factor ratio
}
// Function to convert a value from one unit to another
export function convertUnit(
value: number,
fromUnitId?: string,
toUnitId?: string,
): number | null {
let fromUnit: string | undefined;
let toUnit: string | undefined;
// Finds the category that contains the specified units and extracts fromUnit and toUnit using array methods
const category = dataTypeCategories.find((category) =>
category.formats.some((format) => {
if (format.id === fromUnitId) fromUnit = format.id;
if (format.id === toUnitId) toUnit = format.id;
return fromUnit && toUnit; // Break out early if both units are found
}),
);
if (!category || !fromUnit || !toUnit) return null; // Return null if category or units are not found
// Gets the conversion factor for the specified units
const conversionFactor = getConversionFactor(
fromUnit,
toUnit,
category.name as any,
);
if (conversionFactor === null) return null; // Return null if conversion is not possible
return value * conversionFactor;
}
// Function to get the category name for a given unit ID
export const getCategoryName = (unitId: string): CategoryNames | null => {
// Finds the category that contains the specified unit ID
const foundCategory = dataTypeCategories.find((category) =>
category.formats.some((format) => format.id === unitId),
);
return foundCategory ? (foundCategory.name as CategoryNames) : null;
};

View File

@@ -311,7 +311,6 @@ function RightContainer({
setThresholds={setThresholds}
yAxisUnit={yAxisUnit}
selectedGraph={selectedGraph}
columnUnits={columnUnits}
/>
</section>
)}

View File

@@ -1,4 +1,3 @@
import { DefaultOptionType } from 'antd/es/select';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import {
initialQueryBuilderFormValuesMap,
@@ -9,19 +8,12 @@ import {
listViewInitialTraceQuery,
PANEL_TYPES_INITIAL_QUERY,
} from 'container/NewDashboard/ComponentsSlider/constants';
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
import { cloneDeep, isEmpty, isEqual, set, unset } from 'lodash-es';
import { cloneDeep, isEqual, set, unset } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import {
dataTypeCategories,
getCategoryName,
} from './RightContainer/dataFormatCategories';
import { CategoryNames } from './RightContainer/types';
export const getIsQueryModified = (
currentQuery: Query,
stagedQuery: Query | null,
@@ -537,41 +529,3 @@ export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {
EQueryType.PROM,
],
};
/**
* Retrieves a list of category select options based on the provided category name.
* If the category is found, it maps the formats to an array of objects containing
* the label and value for each format.
*/
export const getCategorySelectOptionByName = (
name?: CategoryNames | string,
): DefaultOptionType[] =>
dataTypeCategories
.find((category) => category.name === name)
?.formats.map((format) => ({
label: format.name,
value: format.id,
})) || [];
/**
* Generates unit options based on the provided column unit.
* It first retrieves the category name associated with the column unit.
* If the category is empty, it maps all supported categories to their respective
* select options. If a valid category is found, it filters the supported categories
* to return only the options for the matched category.
*/
export const unitOptions = (columnUnit: string): DefaultOptionType[] => {
const category = getCategoryName(columnUnit);
if (isEmpty(category)) {
return categoryToSupport.map((category) => ({
label: category,
options: getCategorySelectOptionByName(category),
}));
}
return categoryToSupport
.filter((supportedCategory) => supportedCategory === category)
.map((filteredCategory) => ({
label: filteredCategory,
options: getCategorySelectOptionByName(filteredCategory),
}));
};

View File

@@ -60,7 +60,7 @@ This is a **sample cURL request** which can be used as a template:
&nbsp;
```bash
curl --location 'https://ingest.{{REGION}}.signoz.cloud:443/logs/json' \
curl --location 'https://ingest.{{REGION}}.signoz.cloud:443/logs/json/' \
--header 'Content-Type: application/json' \
--header 'signoz-access-token: {{SIGNOZ_INGESTION_KEY}}' \
--data '[

View File

@@ -13,7 +13,7 @@ import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
import { DataSource } from 'types/common/queryBuilder';
interface FunctionProps {
query: IBuilderQuery;
@@ -57,13 +57,6 @@ export default function Function({
? logsQueryFunctionOptions
: metricQueryFunctionOptions;
const disableRemoveFunction = funcData.name === QueryFunctionsTypes.ANOMALY;
if (funcData.name === QueryFunctionsTypes.ANOMALY) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
return (
<Flex className="query-function">
<Select
@@ -99,7 +92,6 @@ export default function Function({
<Button
className="periscope-btn query-function-delete-btn"
disabled={disableRemoveFunction}
onClick={(): void => {
handleDeleteFunction(funcData, index);
}}

View File

@@ -92,8 +92,6 @@ export default function QueryFunctions({
const isDarkMode = useIsDarkMode();
const hasAnomalyFunction = functions.some((func) => func.name === 'anomaly');
const handleAddNewFunction = (): void => {
const defaultFunctionStruct =
query.dataSource === DataSource.LOGS
@@ -107,22 +105,9 @@ export default function QueryFunctions({
},
];
const functionsCopy = cloneDeep(updatedFunctionsArr);
setFunctions(updatedFunctionsArr);
const anomalyFuncIndex = functionsCopy.findIndex(
(func) => func.name === 'anomaly',
);
if (anomalyFuncIndex !== -1) {
const anomalyFunc = functionsCopy[anomalyFuncIndex];
functionsCopy.splice(anomalyFuncIndex, 1);
functionsCopy.push(anomalyFunc);
}
setFunctions(functionsCopy);
onChange(functionsCopy);
onChange(updatedFunctionsArr);
};
const handleDeleteFunction = (
@@ -196,9 +181,7 @@ export default function QueryFunctions({
<Tooltip
title={
functions && functions.length >= 3 ? (
`Functions are in early access. You can add a maximum of ${
hasAnomalyFunction ? 2 : 3
} function as of now.`
'Functions are in early access. You can add a maximum of 3 function as of now.'
) : (
<div style={{ textAlign: 'center' }}>
Add new function

View File

@@ -59,10 +59,9 @@ export const useOrderByFilter = ({
];
}, [searchText]);
const selectedValue = useMemo(
() => transformToOrderByStringValues(query, entityVersion),
[query, entityVersion],
);
const selectedValue = useMemo(() => transformToOrderByStringValues(query), [
query,
]);
const generateOptions = useCallback(
(options: IOption[]): IOption[] => {

View File

@@ -13,14 +13,11 @@ export const orderByValueDelimiter = '|';
export const transformToOrderByStringValues = (
query: IBuilderQuery,
entityVersion?: string,
): IOption[] => {
const prepareSelectedValue: IOption[] = query.orderBy.map((item) => {
if (item.columnName === SIGNOZ_VALUE) {
return {
label: `${
entityVersion === 'v4' ? query.spaceAggregation : query.aggregateOperator
}(${query.aggregateAttribute.key}) ${item.order}`,
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
}

View File

@@ -1,6 +1,4 @@
/* eslint-disable no-nested-ternary */
import './QueryBuilderSearchV2.styles.scss';
import { Typography } from 'antd';
import {
ArrowDown,

View File

@@ -319,7 +319,7 @@ function QueryBuilderSearchV2(
value: '',
}));
setCurrentState(DropdownState.OPERATOR);
setSearchValue(`${(parsedValue as BaseAutocompleteData)?.key} `);
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
}
} else if (currentState === DropdownState.OPERATOR) {
if (isEmpty(value) && currentFilterItem?.key?.key) {
@@ -360,7 +360,7 @@ function QueryBuilderSearchV2(
value: '',
}));
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
setSearchValue(`${currentFilterItem?.key?.key} ${value} `);
setSearchValue(`${currentFilterItem?.key?.key} ${value}`);
}
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
const operatorType =
@@ -512,6 +512,11 @@ function QueryBuilderSearchV2(
// this useEffect takes care of tokenisation based on the search state
useEffect(() => {
// if we are still fetching the suggestions then return as we won't know the type / data-type etc for the attribute key
if (isFetchingSuggestions) {
return;
}
// if there is no search value reset to the default state
if (!searchValue) {
setCurrentFilterItem(undefined);
@@ -761,7 +766,6 @@ function QueryBuilderSearchV2(
suggestionsData?.payload?.attributes,
]);
// keep the query in sync with the selected tags in logs explorer page
useEffect(() => {
const filterTags: IBuilderQuery['filters'] = {
op: 'AND',
@@ -784,14 +788,16 @@ function QueryBuilderSearchV2(
if (!isEqual(query.filters, filterTags)) {
onChange(filterTags);
setTags(
filterTags.items.map((tag) => ({
...tag,
op: getOperatorFromValue(tag.op),
})) as ITag[],
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tags]);
// keep the use effects pure!
// if the tags lacks the ID then the above use effect will add it to query
// and then the below use effect will take care of adding it to the tags.
// keep the tags in sycn with current query.
useEffect(() => {
// convert the query and tags to same format before comparison
if (!isEqual(getInitTags(query), tags)) {

View File

@@ -98,7 +98,7 @@ function NoFilterTable({
return (
<ResizeTable
columns={columns}
rowKey={(record): string => `${record.startsAt}-${record.fingerprint}`}
rowKey="startsAt"
dataSource={filteredAlerts}
/>
);

View File

@@ -30,12 +30,12 @@
<meta
data-react-helmet="true"
property="og:image"
content="/images/signoz-hero-image.webp"
content="https://signoz.io/img/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="/images/signoz-hero-image.webp"
content="https://signoz.io/img/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
@@ -51,10 +51,26 @@
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<meta name="robots" content="noindex">
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css?family=Fira+Code"
rel="stylesheet"
/>
<link rel="stylesheet" href="/css/uPlot.min.css" />
<link
rel="stylesheet"
href="https://unpkg.com/uplot@1.6.26/dist/uPlot.min.css"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&family=Work+Sans:wght@500&family=Space+Mono&display=swap"
rel="stylesheet"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,11 +1,9 @@
import { themeColors } from 'constants/theme';
import getLabelName from 'lib/getLabelName';
import { colors } from 'lib/getRandomColor';
import { cloneDeep, isUndefined } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData } from 'types/api/widgets/getQuery';
import { generateColor } from './generateColor';
function getXAxisTimestamps(seriesList: QueryData[]): number[] {
const timestamps = new Set();
@@ -97,7 +95,6 @@ export const getUPlotChartData = (
const processAnomalyDetectionData = (
anomalyDetectionData: any,
isDarkMode: boolean,
): Record<string, { data: number[][]; color: string }> => {
if (!anomalyDetectionData) {
return {};
@@ -129,8 +126,7 @@ const processAnomalyDetectionData = (
legend || '',
);
const objKey =
anomalyDetectionData.length > 1 ? `${queryName}-${label}` : label;
const objKey = `${queryName}-${label}`;
processedData[objKey] = {
data: [
@@ -140,10 +136,7 @@ const processAnomalyDetectionData = (
upperBoundSeries[index].values.map((v: { value: number }) => v.value),
lowerBoundSeries[index].values.map((v: { value: number }) => v.value),
],
color: generateColor(
objKey,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
),
color: colors[index],
legendLabel: label,
};
}
@@ -153,8 +146,7 @@ const processAnomalyDetectionData = (
};
export const getUplotChartDataForAnomalyDetection = (
apiResponse: MetricRangePayloadProps,
isDarkMode: boolean,
apiResponse?: MetricRangePayloadProps,
): Record<
string,
{
@@ -164,5 +156,6 @@ export const getUplotChartDataForAnomalyDetection = (
}
> => {
const anomalyDetectionData = apiResponse?.data?.newResult?.data?.result;
return processAnomalyDetectionData(anomalyDetectionData, isDarkMode);
return processAnomalyDetectionData(anomalyDetectionData);
};

View File

@@ -233,47 +233,6 @@ GetYAxisScale): { auto?: boolean; range?: uPlot.Scale.Range } => {
return { auto: false, range: [min, max] };
};
function adjustMinMax(
min: number,
max: number,
): {
adjustedMin: number;
adjustedMax: number;
} {
// Ensure min and max are valid
if (min === -Infinity && max === Infinity) {
return { adjustedMin: -Infinity, adjustedMax: Infinity };
}
const range = max - min;
const adjustment = range * 0.1;
let adjustedMin: number;
let adjustedMax: number;
// Handle the case for -Infinity
if (min === -Infinity) {
adjustedMin = -Infinity;
} else if (min === 0) {
adjustedMin = min - adjustment; // Special case for when min is 0
} else if (min < 0) {
// For negative min, add 10% of the range to bring closer to zero
adjustedMin = min - range * 0.1;
} else {
// For positive min, subtract 10% from min itself
adjustedMin = min - min * 0.1;
}
// Handle the case for Infinity
if (max === Infinity) {
adjustedMax = Infinity;
} else {
adjustedMax = max * 1.1; // Regular case for finite max
}
return { adjustedMin, adjustedMax };
}
function getMinMax(data: any): { minValue: number; maxValue: number } {
// Exclude the first array
const arrays = data.slice(1);
@@ -285,9 +244,7 @@ function getMinMax(data: any): { minValue: number; maxValue: number } {
const minValue = flattened.length ? Math.min(...flattened) : 0;
const maxValue = Math.max(...flattened);
const { adjustedMin, adjustedMax } = adjustMinMax(minValue, maxValue);
return { minValue: adjustedMin, maxValue: adjustedMax };
return { minValue, maxValue };
}
export const getYAxisScaleForAnomalyDetection = ({

View File

@@ -11,19 +11,20 @@
.edit-rules-card {
width: 20rem;
padding: 1rem;
.content {
font-style: normal;
font-weight: 300;
font-size: 18px;
line-height: 20px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
margin: 0;
}
}
.content {
font-style: normal;
font-weight: 300;
font-size: 18px;
line-height: 20px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
margin: 0;
}
.btn-container {
display: flex;
justify-content: center;

View File

@@ -2,6 +2,8 @@ import './Integrations.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Flex, Input, Typography } from 'antd';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { integrationsListMessage } from 'components/LaunchChatSupport/util';
import { Search } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
@@ -23,6 +25,13 @@ function Header(props: HeaderProps): JSX.Element {
<Typography.Text className="subtitle">
Manage Integrations for this workspace
</Typography.Text>
<LaunchChatSupport
attributes={{ screen: 'Integrations list page' }}
eventName="Integrations: Facing issues in integrations"
buttonText="Facing issues with integrations"
message={integrationsListMessage}
onHoverText="Click here to get help with integrations"
/>
</Flex>
<Input

View File

@@ -5,6 +5,8 @@ import './IntegrationDetailPage.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Flex, Skeleton, Typography } from 'antd';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { integrationDetailMessage } from 'components/LaunchChatSupport/util';
import { useGetIntegration } from 'hooks/Integrations/useGetIntegration';
import { useGetIntegrationStatus } from 'hooks/Integrations/useGetIntegrationStatus';
import { defaultTo } from 'lodash-es';
@@ -75,6 +77,18 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
>
All Integrations
</Button>
<LaunchChatSupport
attributes={{
screen: 'Integrations detail page',
activeTab: activeDetailTab,
integrationTitle: integrationData?.title || '',
integrationId: selectedIntegration,
}}
eventName="Integrations: Facing issues in integrations"
buttonText="Facing issues with integration"
message={integrationDetailMessage(selectedIntegration)}
onHoverText="Click here to get help with this integration"
/>
</Flex>
{loading ? (

View File

@@ -15,6 +15,7 @@
font-style: normal;
line-height: 28px; /* 155.556% */
letter-spacing: -0.09px;
font-family: Inter;
font-weight: 500;
}
@@ -24,6 +25,7 @@
font-style: normal;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
font-family: Inter;
font-weight: 400;
}
@@ -127,6 +129,7 @@
.heading {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
@@ -137,6 +140,7 @@
.description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
@@ -159,6 +163,7 @@
background: var(--bg-ink-200);
box-shadow: none;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;

View File

@@ -10,7 +10,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
export const KAFKA_SETUP_DOC_LINK =
'https://signoz.io/docs/messaging-queues/kafka?utm_source=product&utm_medium=kafka-get-started';
'https://github.com/shivanshuraj1333/kafka-opentelemetry-instrumentation/tree/master';
export function convertToTitleCase(text: string): string {
return text

View File

@@ -303,6 +303,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
return (
loading ||
!values.email ||
!values.organizationName ||
(!precheck.sso && (!values.password || !values.confirmPassword)) ||
(!isDetailsDisable && !values.firstName) ||
confirmPasswordError ||
@@ -353,6 +354,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
<FormContainer.Item noStyle name="organizationName">
<Input
placeholder={t('placeholder_orgname')}
required
id="organizationName"
disabled={isDetailsDisable}
/>

View File

@@ -274,28 +274,3 @@ notifications - 2050
url('../public/fonts/GeistMonoVF.woff2') format('woff');
/* Add other formats if needed (e.g., woff2, truetype, opentype, svg) */
}
@font-face {
font-family: 'Inter';
src: url('../public/fonts/Inter-VariableFont_opsz,wght.ttf') format('truetype');
font-weight: 300 700;
font-style: normal;
}
@font-face {
font-family: 'Work Sans';
src: url('../public/fonts/WorkSans-VariableFont_wght.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Space Mono';
src: url('../public/fonts/SpaceMono-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Fira Code';
src: url('../public/fonts/FiraCode-VariableFont_wght.ttf') format('truetype');
font-weight: 300 700;
font-style: normal;
}

View File

@@ -1,39 +0,0 @@
export interface GetOrgPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface GetUserPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface GetAllOrgPreferencesResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface GetAllUserPreferencesResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface UpdateOrgPreferenceProps {
key: string;
value: unknown;
}
export interface UpdateUserPreferenceProps {
key: string;
value: unknown;
}
export interface UpdateOrgPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface UpdateUserPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}

View File

@@ -50,7 +50,6 @@ export type OrderByPayload = {
export interface QueryFunctionProps {
name: string;
args: string[];
namedArgs?: Record<string, any>;
}
// Type for query builder

View File

@@ -1,6 +1,5 @@
import { orange } from '@ant-design/colors';
import { Color } from '@signozhq/design-tokens';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
export const getDefaultLogBackground = (
isReadOnly?: boolean,
@@ -18,28 +17,10 @@ export const getDefaultLogBackground = (
export const getActiveLogBackground = (
isActiveLog = true,
isDarkMode = true,
logType?: string,
): string => {
if (!isActiveLog) return ``;
if (isDarkMode) {
switch (logType) {
case LogType.INFO:
return `background-color: ${Color.BG_ROBIN_500}10 !important;`;
case LogType.WARN:
return `background-color: ${Color.BG_AMBER_500}10 !important;`;
case LogType.ERROR:
return `background-color: ${Color.BG_CHERRY_500}10 !important;`;
case LogType.TRACE:
return `background-color: ${Color.BG_FOREST_400}10 !important;`;
case LogType.DEBUG:
return `background-color: ${Color.BG_AQUA_500}10 !important;`;
case LogType.FATAL:
return `background-color: ${Color.BG_SAKURA_500}10 !important;`;
default:
return `background-color: ${Color.BG_SLATE_200} !important;`;
}
}
return `background-color: ${Color.BG_VANILLA_300}!important; color: ${Color.TEXT_SLATE_400} !important;`;
if (isDarkMode) return `background-color: ${Color.BG_SLATE_200};`;
return `background-color: ${Color.BG_VANILLA_300}; color: ${Color.TEXT_SLATE_400}`;
};
export const getHightLightedLogBackground = (

View File

@@ -6139,7 +6139,7 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
braces@^3.0.3, braces@~3.0.2:
braces@^3.0.2, braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
@@ -12123,11 +12123,11 @@ micromark@^3.0.0:
uvu "^0.5.0"
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
version "4.0.5"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
dependencies:
braces "^3.0.3"
braces "^3.0.2"
picomatch "^2.3.1"
microseconds@0.2.0:

2
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.25.0
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.12
github.com/SigNoz/signoz-otel-collector v0.102.10
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
View File

@@ -70,8 +70,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.12.0 h1:+BXeIHyMOOWWa+xjhJ+x80JFva7r1WzWIfIhQ5PUmIE=
github.com/SigNoz/prometheus v1.12.0/go.mod h1:EqNM27OwmPfqMUk+E+XG1L9rfDFcyXnzzDrg0EPOfxA=
github.com/SigNoz/signoz-otel-collector v0.102.12 h1:5yY0IBtNz6SHMzKzwHmKfIx99Ij8mr72nDI2Xi08pDQ=
github.com/SigNoz/signoz-otel-collector v0.102.12/go.mod h1:tcNyU+NSn7ZkzZcLa+k+dJIPOPV+CjHn3+z1SICAfdA=
github.com/SigNoz/signoz-otel-collector v0.102.10 h1:1zjU31OcRZL6fS0IIag8LA8bdhP4S28dzovDwuOg7Lg=
github.com/SigNoz/signoz-otel-collector v0.102.10/go.mod h1:APoBVD4aRu9vIny1vdzZSi2wPY3elyjHA/I/rh1hKfs=
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=

View File

@@ -13,7 +13,7 @@ https://github.com/SigNoz/signoz/blob/main/CONTRIBUTING.md#to-run-clickhouse-set
- Change the alertmanager section in `signoz/deploy/docker/clickhouse-setup/docker-compose.yaml` as follows:
```console
alertmanager:
image: signoz/alertmanager:0.23.7
image: signoz/alertmanager:0.23.5
volumes:
- ./data/alertmanager:/data
expose:

View File

@@ -3560,7 +3560,7 @@ func (r *ClickHouseReader) AggregateLogs(ctx context.Context, params *model.Logs
}
func (r *ClickHouseReader) QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error) {
var result = model.DashboardVar{VariableValues: make([]interface{}, 0)}
var result model.DashboardVar
rows, err := r.db.Query(ctx, query)
zap.L().Info(query)

View File

@@ -112,12 +112,7 @@ type APIHandler struct {
UseLogsNewSchema bool
hostsRepo *inframetrics.HostsRepo
processesRepo *inframetrics.ProcessesRepo
podsRepo *inframetrics.PodsRepo
nodesRepo *inframetrics.NodesRepo
namespacesRepo *inframetrics.NamespacesRepo
clustersRepo *inframetrics.ClustersRepo
hostsRepo *inframetrics.HostsRepo
}
type APIHandlerOpts struct {
@@ -188,11 +183,6 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
querierv2 := querierV2.NewQuerier(querierOptsV2)
hostsRepo := inframetrics.NewHostsRepo(opts.Reader, querierv2)
processesRepo := inframetrics.NewProcessesRepo(opts.Reader, querierv2)
podsRepo := inframetrics.NewPodsRepo(opts.Reader, querierv2)
nodesRepo := inframetrics.NewNodesRepo(opts.Reader, querierv2)
namespacesRepo := inframetrics.NewNamespacesRepo(opts.Reader, querierv2)
clustersRepo := inframetrics.NewClustersRepo(opts.Reader, querierv2)
aH := &APIHandler{
reader: opts.Reader,
@@ -212,11 +202,6 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
querierV2: querierv2,
UseLogsNewSchema: opts.UseLogsNewSchema,
hostsRepo: hostsRepo,
processesRepo: processesRepo,
podsRepo: podsRepo,
nodesRepo: nodesRepo,
namespacesRepo: namespacesRepo,
clustersRepo: clustersRepo,
}
logsQueryBuilder := logsv3.PrepareLogsQuery
@@ -366,35 +351,10 @@ func (aH *APIHandler) RegisterQueryRangeV3Routes(router *mux.Router, am *AuthMid
}
func (aH *APIHandler) RegisterInfraMetricsRoutes(router *mux.Router, am *AuthMiddleware) {
hostsSubRouter := router.PathPrefix("/api/v1/hosts").Subrouter()
hostsSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getHostAttributeKeys)).Methods(http.MethodGet)
hostsSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getHostAttributeValues)).Methods(http.MethodGet)
hostsSubRouter.HandleFunc("/list", am.ViewAccess(aH.getHostList)).Methods(http.MethodPost)
processesSubRouter := router.PathPrefix("/api/v1/processes").Subrouter()
processesSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getProcessAttributeKeys)).Methods(http.MethodGet)
processesSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getProcessAttributeValues)).Methods(http.MethodGet)
processesSubRouter.HandleFunc("/list", am.ViewAccess(aH.getProcessList)).Methods(http.MethodPost)
podsSubRouter := router.PathPrefix("/api/v1/pods").Subrouter()
podsSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getPodAttributeKeys)).Methods(http.MethodGet)
podsSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getPodAttributeValues)).Methods(http.MethodGet)
podsSubRouter.HandleFunc("/list", am.ViewAccess(aH.getPodList)).Methods(http.MethodPost)
nodesSubRouter := router.PathPrefix("/api/v1/nodes").Subrouter()
nodesSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getNodeAttributeKeys)).Methods(http.MethodGet)
nodesSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getNodeAttributeValues)).Methods(http.MethodGet)
nodesSubRouter.HandleFunc("/list", am.ViewAccess(aH.getNodeList)).Methods(http.MethodPost)
namespacesSubRouter := router.PathPrefix("/api/v1/namespaces").Subrouter()
namespacesSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getNamespaceAttributeKeys)).Methods(http.MethodGet)
namespacesSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getNamespaceAttributeValues)).Methods(http.MethodGet)
namespacesSubRouter.HandleFunc("/list", am.ViewAccess(aH.getNamespaceList)).Methods(http.MethodPost)
clustersSubRouter := router.PathPrefix("/api/v1/clusters").Subrouter()
clustersSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getClusterAttributeKeys)).Methods(http.MethodGet)
clustersSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getClusterAttributeValues)).Methods(http.MethodGet)
clustersSubRouter.HandleFunc("/list", am.ViewAccess(aH.getClusterList)).Methods(http.MethodPost)
subRouter := router.PathPrefix("/api/v1/hosts").Subrouter()
subRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getHostAttributeKeys)).Methods(http.MethodGet)
subRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getHostAttributeValues)).Methods(http.MethodGet)
subRouter.HandleFunc("/list", am.ViewAccess(aH.getHostList)).Methods(http.MethodPost)
}
func (aH *APIHandler) RegisterWebSocketPaths(router *mux.Router, am *AuthMiddleware) {
@@ -443,11 +403,11 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
router.HandleFunc("/api/v1/rules/{id}/history/top_contributors", am.ViewAccess(aH.getRuleStateHistoryTopContributors)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/rules/{id}/history/overall_status", am.ViewAccess(aH.getOverallStateTransitions)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/downtime_schedules", am.ViewAccess(aH.listDowntimeSchedules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.ViewAccess(aH.getDowntimeSchedule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/downtime_schedules", am.EditAccess(aH.createDowntimeSchedule)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.editDowntimeSchedule)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.deleteDowntimeSchedule)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/downtime_schedules", am.OpenAccess(aH.listDowntimeSchedules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.OpenAccess(aH.getDowntimeSchedule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/downtime_schedules", am.OpenAccess(aH.createDowntimeSchedule)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.OpenAccess(aH.editDowntimeSchedule)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.OpenAccess(aH.deleteDowntimeSchedule)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/dashboards", am.ViewAccess(aH.getDashboards)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/dashboards", am.EditAccess(aH.createDashboards)).Methods(http.MethodPost)
@@ -518,7 +478,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
// === Authentication APIs ===
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/invite/bulk", am.AdminAccess(aH.inviteUsers)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/invite/{email}", am.AdminAccess(aH.revokeInvite)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.listPendingInvites)).Methods(http.MethodGet)
@@ -2009,32 +1968,6 @@ func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) {
aH.WriteJSON(w, r, resp)
}
func (aH *APIHandler) inviteUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseInviteUsersRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
response, err := auth.InviteUsers(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
// Check the response status and set the appropriate HTTP status code
if response.Status == "failure" {
w.WriteHeader(http.StatusBadRequest) // 400 Bad Request for failure
} else if response.Status == "partial_success" {
w.WriteHeader(http.StatusPartialContent) // 206 Partial Content
} else {
w.WriteHeader(http.StatusOK) // 200 OK for success
}
aH.WriteJSON(w, r, response)
}
// getInvite returns the invite object details for the given invite token. We do not need to
// protect this API because invite token itself is meant to be private.
func (aH *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
@@ -2548,35 +2481,22 @@ func (aH *APIHandler) RegisterMessagingQueuesRoutes(router *mux.Router, am *Auth
// SubRouter for kafka
kafkaRouter := router.PathPrefix("/api/v1/messaging-queues/kafka").Subrouter()
onboardingRouter := kafkaRouter.PathPrefix("/onboarding").Subrouter()
onboardingRouter.HandleFunc("/producers", am.ViewAccess(aH.onboardProducers)).Methods(http.MethodPost)
onboardingRouter.HandleFunc("/consumers", am.ViewAccess(aH.onboardConsumers)).Methods(http.MethodPost)
onboardingRouter.HandleFunc("/kafka", am.ViewAccess(aH.onboardKafka)).Methods(http.MethodPost)
partitionLatency := kafkaRouter.PathPrefix("/partition-latency").Subrouter()
partitionLatency.HandleFunc("/overview", am.ViewAccess(aH.getPartitionOverviewLatencyData)).Methods(http.MethodPost)
partitionLatency.HandleFunc("/consumer", am.ViewAccess(aH.getConsumerPartitionLatencyData)).Methods(http.MethodPost)
consumerLagRouter := kafkaRouter.PathPrefix("/consumer-lag").Subrouter()
consumerLagRouter.HandleFunc("/producer-details", am.ViewAccess(aH.getProducerData)).Methods(http.MethodPost)
consumerLagRouter.HandleFunc("/consumer-details", am.ViewAccess(aH.getConsumerData)).Methods(http.MethodPost)
consumerLagRouter.HandleFunc("/network-latency", am.ViewAccess(aH.getNetworkData)).Methods(http.MethodPost)
topicThroughput := kafkaRouter.PathPrefix("/topic-throughput").Subrouter()
topicThroughput.HandleFunc("/producer", am.ViewAccess(aH.getProducerThroughputOverview)).Methods(http.MethodPost)
topicThroughput.HandleFunc("/producer-details", am.ViewAccess(aH.getProducerThroughputDetails)).Methods(http.MethodPost)
topicThroughput.HandleFunc("/consumer", am.ViewAccess(aH.getConsumerThroughputOverview)).Methods(http.MethodPost)
topicThroughput.HandleFunc("/consumer-details", am.ViewAccess(aH.getConsumerThroughputDetails)).Methods(http.MethodPost)
spanEvaluation := kafkaRouter.PathPrefix("/span").Subrouter()
spanEvaluation.HandleFunc("/evaluation", am.ViewAccess(aH.getProducerConsumerEval)).Methods(http.MethodPost)
onboardingRouter := kafkaRouter.PathPrefix("/onboarding").Subrouter()
onboardingRouter.HandleFunc("/producers", am.ViewAccess(aH.onboardProducers)).Methods(http.MethodPost)
onboardingRouter.HandleFunc("/consumers", am.ViewAccess(aH.onboardConsumers)).Methods(http.MethodPost)
onboardingRouter.HandleFunc("/kafka", am.ViewAccess(aH.onboardKafka)).Methods(http.MethodPost)
// for other messaging queues, add SubRouters here
}
// not using md5 hashing as the plain string would work
func uniqueIdentifier(params []string, separator string) string {
return strings.Join(params, separator)
func uniqueIdentifier(clientID, serviceInstanceID, serviceName, separator string) string {
return clientID + separator + serviceInstanceID + separator + serviceName
}
func (aH *APIHandler) onboardProducers(
@@ -2919,7 +2839,7 @@ func (aH *APIHandler) getNetworkData(
return
}
queryRangeParams, err := mq.BuildQRParamsWithCache(messagingQueue, "throughput", attributeCache)
queryRangeParams, err := mq.BuildQRParamsNetwork(messagingQueue, "throughput", attributeCache)
if err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
@@ -2946,8 +2866,7 @@ func (aH *APIHandler) getNetworkData(
clientID, clientIDOk := series.Labels["client_id"]
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
serviceName, serviceNameOk := series.Labels["service_name"]
params := []string{clientID, serviceInstanceID, serviceName}
hashKey := uniqueIdentifier(params, "#")
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
_, ok := attributeCache.Hash[hashKey]
if clientIDOk && serviceInstanceIDOk && serviceNameOk && !ok {
attributeCache.Hash[hashKey] = struct{}{}
@@ -2958,7 +2877,7 @@ func (aH *APIHandler) getNetworkData(
}
}
queryRangeParams, err = mq.BuildQRParamsWithCache(messagingQueue, "fetch-latency", attributeCache)
queryRangeParams, err = mq.BuildQRParamsNetwork(messagingQueue, "fetch-latency", attributeCache)
if err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
@@ -2984,8 +2903,7 @@ func (aH *APIHandler) getNetworkData(
clientID, clientIDOk := series.Labels["client_id"]
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
serviceName, serviceNameOk := series.Labels["service_name"]
params := []string{clientID, serviceInstanceID, serviceName}
hashKey := uniqueIdentifier(params, "#")
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
_, ok := attributeCache.Hash[hashKey]
if clientIDOk && serviceInstanceIDOk && serviceNameOk && ok {
latencySeries = append(latencySeries, series)
@@ -3087,362 +3005,6 @@ func (aH *APIHandler) getConsumerData(
aH.Respond(w, resp)
}
// s1
func (aH *APIHandler) getPartitionOverviewLatencyData(
w http.ResponseWriter, r *http.Request,
) {
messagingQueue, apiErr := ParseMessagingQueueBody(r)
if apiErr != nil {
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "producer-topic-throughput")
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 errQuriesByName map[string]error
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
result = postprocess.TransformToTableForClickHouseQueries(result)
resp := v3.QueryRangeResponse{
Result: result,
}
aH.Respond(w, resp)
}
// s1
func (aH *APIHandler) getConsumerPartitionLatencyData(
w http.ResponseWriter, r *http.Request,
) {
messagingQueue, apiErr := ParseMessagingQueueBody(r)
if apiErr != nil {
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "consumer_partition_latency")
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 errQuriesByName map[string]error
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
result = postprocess.TransformToTableForClickHouseQueries(result)
resp := v3.QueryRangeResponse{
Result: result,
}
aH.Respond(w, resp)
}
// s3 p overview
// fetch traces
// cache attributes
// fetch byte rate metrics
func (aH *APIHandler) getProducerThroughputOverview(
w http.ResponseWriter, r *http.Request,
) {
messagingQueue, apiErr := ParseMessagingQueueBody(r)
if apiErr != nil {
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
attributeCache := &mq.Clients{
Hash: make(map[string]struct{}),
}
queryRangeParams, err := mq.BuildQRParamsWithCache(messagingQueue, "producer-throughput-overview", 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 errQuriesByName map[string]error
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
for _, res := range result {
for _, series := range res.Series {
serviceName, serviceNameOk := series.Labels["service_name"]
topicName, topicNameOk := series.Labels["topic"]
params := []string{serviceName, topicName}
hashKey := uniqueIdentifier(params, "#")
_, ok := attributeCache.Hash[hashKey]
if topicNameOk && serviceNameOk && !ok {
attributeCache.Hash[hashKey] = struct{}{}
attributeCache.TopicName = append(attributeCache.TopicName, topicName)
attributeCache.ServiceName = append(attributeCache.ServiceName, serviceName)
}
}
}
queryRangeParams, err = mq.BuildQRParamsWithCache(messagingQueue, "producer-throughput-overview-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)
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 {
topic, topicOk := series.Labels["topic"]
serviceName, serviceNameOk := series.Labels["service_name"]
params := []string{topic, serviceName}
hashKey := uniqueIdentifier(params, "#")
_, ok := attributeCache.Hash[hashKey]
if topicOk && 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)
}
// s3 p details
func (aH *APIHandler) getProducerThroughputDetails(
w http.ResponseWriter, r *http.Request,
) {
messagingQueue, apiErr := ParseMessagingQueueBody(r)
if apiErr != nil {
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "producer-throughput-details")
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 errQuriesByName map[string]error
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
result = postprocess.TransformToTableForClickHouseQueries(result)
resp := v3.QueryRangeResponse{
Result: result,
}
aH.Respond(w, resp)
}
// s3 c overview
func (aH *APIHandler) getConsumerThroughputOverview(
w http.ResponseWriter, r *http.Request,
) {
messagingQueue, apiErr := ParseMessagingQueueBody(r)
if apiErr != nil {
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "consumer-throughput-overview")
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 errQuriesByName map[string]error
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
result = postprocess.TransformToTableForClickHouseQueries(result)
resp := v3.QueryRangeResponse{
Result: result,
}
aH.Respond(w, resp)
}
// s3 c details
func (aH *APIHandler) getConsumerThroughputDetails(
w http.ResponseWriter, r *http.Request,
) {
messagingQueue, apiErr := ParseMessagingQueueBody(r)
if apiErr != nil {
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "consumer-throughput-details")
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 errQuriesByName map[string]error
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
result = postprocess.TransformToTableForClickHouseQueries(result)
resp := v3.QueryRangeResponse{
Result: result,
}
aH.Respond(w, resp)
}
// s4
// needs logic to parse duration
// needs logic to get the percentage
// show 10 traces
func (aH *APIHandler) getProducerConsumerEval(
w http.ResponseWriter, r *http.Request,
) {
messagingQueue, apiErr := ParseMessagingQueueBody(r)
if apiErr != nil {
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "producer-consumer-eval")
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 errQuriesByName map[string]error
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
resp := v3.QueryRangeResponse{
Result: result,
}
aH.Respond(w, resp)
}
// ParseMessagingQueueBody parse for messaging queue params
func ParseMessagingQueueBody(r *http.Request) (*mq.MessagingQueue, *model.ApiError) {
messagingQueue := new(mq.MessagingQueue)

View File

@@ -69,268 +69,3 @@ func (aH *APIHandler) getHostList(w http.ResponseWriter, r *http.Request) {
// write response
aH.Respond(w, hostList)
}
func (aH *APIHandler) getProcessAttributeKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeKeyRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
keys, err := aH.processesRepo.GetProcessAttributeKeys(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) getProcessAttributeValues(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeValueRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
values, err := aH.processesRepo.GetProcessAttributeValues(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) getProcessList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := model.ProcessListRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
hostList, err := aH.processesRepo.GetProcessList(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, hostList)
}
func (aH *APIHandler) getPodAttributeKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeKeyRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
keys, err := aH.podsRepo.GetPodAttributeKeys(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) getPodAttributeValues(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeValueRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
values, err := aH.podsRepo.GetPodAttributeValues(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) getPodList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := model.PodListRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
podList, err := aH.podsRepo.GetPodList(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, podList)
}
func (aH *APIHandler) getNodeAttributeKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeKeyRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
keys, err := aH.nodesRepo.GetNodeAttributeKeys(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) getNodeAttributeValues(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeValueRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
values, err := aH.nodesRepo.GetNodeAttributeValues(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) getNodeList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := model.NodeListRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
nodeList, err := aH.nodesRepo.GetNodeList(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, nodeList)
}
func (aH *APIHandler) getNamespaceAttributeKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeKeyRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
keys, err := aH.namespacesRepo.GetNamespaceAttributeKeys(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) getNamespaceAttributeValues(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeValueRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
values, err := aH.namespacesRepo.GetNamespaceAttributeValues(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) getNamespaceList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := model.NamespaceListRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
namespaceList, err := aH.namespacesRepo.GetNamespaceList(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, namespaceList)
}
func (aH *APIHandler) getClusterAttributeKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeKeyRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
keys, err := aH.clustersRepo.GetClusterAttributeKeys(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) getClusterAttributeValues(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeValueRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
values, err := aH.clustersRepo.GetClusterAttributeValues(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) getClusterList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := model.ClusterListRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
clusterList, err := aH.clustersRepo.GetClusterList(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, clusterList)
}

View File

@@ -1,342 +0,0 @@
package inframetrics
import (
"context"
"math"
"sort"
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
"go.signoz.io/signoz/pkg/query-service/common"
"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/postprocess"
"golang.org/x/exp/slices"
)
var (
metricToUseForClusters = "k8s_node_cpu_utilization"
clusterAttrsToEnrich = []string{"k8s_cluster_name"}
k8sClusterUIDAttrKey = "k8s_cluster_uid"
queryNamesForClusters = map[string][]string{
"cpu": {"A"},
"cpu_allocatable": {"B"},
"memory": {"C"},
"memory_allocatable": {"D"},
}
clusterQueryNames = []string{"A", "B", "C", "D"}
)
type ClustersRepo struct {
reader interfaces.Reader
querierV2 interfaces.Querier
}
func NewClustersRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *ClustersRepo {
return &ClustersRepo{reader: reader, querierV2: querierV2}
}
func (n *ClustersRepo) GetClusterAttributeKeys(ctx context.Context, req v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForClusters
if req.Limit == 0 {
req.Limit = 50
}
attributeKeysResponse, err := n.reader.GetMetricAttributeKeys(ctx, &req)
if err != nil {
return nil, err
}
return attributeKeysResponse, nil
}
func (n *ClustersRepo) GetClusterAttributeValues(ctx context.Context, req v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForClusters
if req.Limit == 0 {
req.Limit = 50
}
attributeValuesResponse, err := n.reader.GetMetricAttributeValues(ctx, &req)
if err != nil {
return nil, err
}
return attributeValuesResponse, nil
}
func (p *ClustersRepo) getMetadataAttributes(ctx context.Context, req model.ClusterListRequest) (map[string]map[string]string, error) {
clusterAttrs := map[string]map[string]string{}
for _, key := range clusterAttrsToEnrich {
hasKey := false
for _, groupByKey := range req.GroupBy {
if groupByKey.Key == key {
hasKey = true
break
}
}
if !hasKey {
req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: key})
}
}
mq := v3.BuilderQuery{
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricToUseForClusters,
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
GroupBy: req.GroupBy,
}
query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq)
if err != nil {
return nil, err
}
query = localQueryToDistributedQuery(query)
attrsListResponse, err := p.reader.GetListResultV3(ctx, query)
if err != nil {
return nil, err
}
for _, row := range attrsListResponse {
stringData := map[string]string{}
for key, value := range row.Data {
if str, ok := value.(string); ok {
stringData[key] = str
} else if strPtr, ok := value.(*string); ok {
stringData[key] = *strPtr
}
}
clusterUID := stringData[k8sClusterUIDAttrKey]
if _, ok := clusterAttrs[clusterUID]; !ok {
clusterAttrs[clusterUID] = map[string]string{}
}
for _, key := range req.GroupBy {
clusterAttrs[clusterUID][key.Key] = stringData[key.Key]
}
}
return clusterAttrs, nil
}
func (p *ClustersRepo) getTopClusterGroups(ctx context.Context, req model.ClusterListRequest, q *v3.QueryRangeParamsV3) ([]map[string]string, []map[string]string, error) {
step, timeSeriesTableName, samplesTableName := getParamsForTopClusters(req)
queryNames := queryNamesForClusters[req.OrderBy.ColumnName]
topClusterGroupsQueryRangeParams := &v3.QueryRangeParamsV3{
Start: req.Start,
End: req.End,
Step: step,
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{},
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
},
}
for _, queryName := range queryNames {
query := q.CompositeQuery.BuilderQueries[queryName].Clone()
query.StepInterval = step
query.MetricTableHints = &v3.MetricTableHints{
TimeSeriesTableName: timeSeriesTableName,
SamplesTableName: samplesTableName,
}
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
topClusterGroupsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, topClusterGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topClusterGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 {
return nil, nil, nil
}
if req.OrderBy.Order == v3.DirectionDesc {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value
})
} else {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value
})
}
max := math.Min(float64(req.Offset+req.Limit), float64(len(formattedResponse[0].Series)))
paginatedTopClusterGroupsSeries := formattedResponse[0].Series[req.Offset:int(max)]
topClusterGroups := []map[string]string{}
for _, series := range paginatedTopClusterGroupsSeries {
topClusterGroups = append(topClusterGroups, series.Labels)
}
allClusterGroups := []map[string]string{}
for _, series := range formattedResponse[0].Series {
allClusterGroups = append(allClusterGroups, series.Labels)
}
return topClusterGroups, allClusterGroups, nil
}
func (p *ClustersRepo) GetClusterList(ctx context.Context, req model.ClusterListRequest) (model.ClusterListResponse, error) {
resp := model.ClusterListResponse{}
if req.Limit == 0 {
req.Limit = 10
}
if req.OrderBy == nil {
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
}
if req.GroupBy == nil {
req.GroupBy = []v3.AttributeKey{{Key: k8sClusterUIDAttrKey}}
resp.Type = model.ResponseTypeList
} else {
resp.Type = model.ResponseTypeGroupedList
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := NodesTableListQuery.Clone()
query.Start = req.Start
query.End = req.End
query.Step = step
for _, query := range query.CompositeQuery.BuilderQueries {
query.StepInterval = step
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
query.GroupBy = req.GroupBy
}
clusterAttrs, err := p.getMetadataAttributes(ctx, req)
if err != nil {
return resp, err
}
topClusterGroups, allClusterGroups, err := p.getTopClusterGroups(ctx, req, query)
if err != nil {
return resp, err
}
groupFilters := map[string][]string{}
for _, topClusterGroup := range topClusterGroups {
for k, v := range topClusterGroup {
groupFilters[k] = append(groupFilters[k], v)
}
}
for groupKey, groupValues := range groupFilters {
hasGroupFilter := false
if req.Filters != nil && len(req.Filters.Items) > 0 {
for _, filter := range req.Filters.Items {
if filter.Key.Key == groupKey {
hasGroupFilter = true
break
}
}
}
if !hasGroupFilter {
for _, query := range query.CompositeQuery.BuilderQueries {
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
Key: v3.AttributeKey{Key: groupKey},
Value: groupValues,
Operator: v3.FilterOperatorIn,
})
}
}
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, query)
if err != nil {
return resp, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, query)
if err != nil {
return resp, err
}
records := []model.ClusterListRecord{}
for _, result := range formattedResponse {
for _, row := range result.Table.Rows {
record := model.ClusterListRecord{
CPUUsage: -1,
CPUAllocatable: -1,
MemoryUsage: -1,
MemoryAllocatable: -1,
}
if clusterUID, ok := row.Data[k8sClusterUIDAttrKey].(string); ok {
record.ClusterUID = clusterUID
}
if cpu, ok := row.Data["A"].(float64); ok {
record.CPUUsage = cpu
}
if cpuAllocatable, ok := row.Data["B"].(float64); ok {
record.CPUAllocatable = cpuAllocatable
}
if mem, ok := row.Data["C"].(float64); ok {
record.MemoryUsage = mem
}
if memoryAllocatable, ok := row.Data["D"].(float64); ok {
record.MemoryAllocatable = memoryAllocatable
}
record.Meta = map[string]string{}
if _, ok := clusterAttrs[record.ClusterUID]; ok {
record.Meta = clusterAttrs[record.ClusterUID]
}
for k, v := range row.Data {
if slices.Contains(clusterQueryNames, k) {
continue
}
if labelValue, ok := v.(string); ok {
record.Meta[k] = labelValue
}
}
records = append(records, record)
}
}
resp.Total = len(allClusterGroups)
resp.Records = records
return resp, nil
}

View File

@@ -1,81 +0,0 @@
package inframetrics
import (
"strings"
"time"
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/model"
)
// getParamsForTopItems returns the step, time series table name and samples table name
// for the top items query. what are we doing here?
// we want to identify the top hosts/pods/nodes quickly, so we use pre-aggregated data
// for samples and time series tables to speed up the query
// the speed of the query depends on the number of values in group by clause, the higher
// the step interval, the faster the query will be as number of rows to group by is reduced
// here we are using the averaged value of the time series data to get the top items
func getParamsForTopItems(start, end int64) (int64, string, string) {
var step int64
var timeSeriesTableName string
var samplesTableName string
if end-start < time.Hour.Milliseconds() {
// 5 minute aggregation for any query less than 1 hour
step = 5 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME
} else if end-start < time.Hour.Milliseconds()*6 {
// 15 minute aggregation for any query less than 6 hours
step = 15 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_6HRS_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME
} else if end-start < time.Hour.Milliseconds()*24 {
// 1 hour aggregation for any query less than 1 day
step = 60 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME
} else if end-start < time.Hour.Milliseconds()*7 {
// 6 hours aggregation for any query less than 1 week
step = 6 * 60 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME
} else {
// 12 hours aggregation for any query greater than 1 week
step = 12 * 60 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME
}
return step, timeSeriesTableName, samplesTableName
}
func getParamsForTopHosts(req model.HostListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
func getParamsForTopPods(req model.PodListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
func getParamsForTopNodes(req model.NodeListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
func getParamsForTopNamespaces(req model.NamespaceListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
func getParamsForTopClusters(req model.ClusterListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
// TODO(srikanthccv): remove this
// What is happening here?
// The `PrepareTimeseriesFilterQuery` uses the local time series table for sub-query because each fingerprint
// goes to same shard.
// However, in this case, we are interested in the attributes values across all the shards.
// So, we replace the local time series table with the distributed time series table.
// See `PrepareTimeseriesFilterQuery` for more details.
func localQueryToDistributedQuery(query string) string {
return strings.Replace(query, ".time_series_v4", ".distributed_time_series_v4", 1)
}

View File

@@ -2,10 +2,12 @@ package inframetrics
import (
"context"
"sort"
"fmt"
"math"
"strings"
"time"
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
@@ -19,45 +21,23 @@ type HostsRepo struct {
querierV2 interfaces.Querier
}
var (
// we don't have a way to get the resource attributes from the current time series table
// but we only want to suggest resource attributes for system metrics,
// this is a list of attributes that we skip from all labels as they are data point attributes
// TODO(srikanthccv): remove this once we have a way to get resource attributes
pointAttrsToIgnore = []string{
"state",
"cpu",
"device",
"direction",
"mode",
"mountpoint",
"type",
"os_type",
"process_cgroup",
"process_command",
"process_command_line",
"process_executable_name",
"process_executable_path",
"process_owner",
"process_parent_pid",
"process_pid",
}
queryNamesForTopHosts = map[string][]string{
"cpu": {"A", "B", "F1"},
"memory": {"C", "D", "F2"},
"wait": {"E", "F", "F3"},
"load15": {"G"},
}
// TODO(srikanthccv): remove hardcoded metric name and support keys from any system metric
metricToUseForHostAttributes = "system_cpu_load_average_15m"
hostNameAttrKey = "host_name"
// TODO(srikanthccv): remove k8s hacky logic from hosts repo after charts users are migrated
k8sNodeNameAttrKey = "k8s_node_name"
agentNameToIgnore = "k8s-infra-otel-agent"
)
var pointAttrsToIgnore = []string{
"state",
"cpu",
"device",
"direction",
"mode",
"mountpoint",
"type",
"process.cgroup",
"process.command",
"process.command_line",
"process.executable.name",
"process.executable.path",
"process.owner",
"process.parent_pid",
"process.pid",
}
func NewHostsRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *HostsRepo {
return &HostsRepo{reader: reader, querierV2: querierV2}
@@ -66,7 +46,7 @@ func NewHostsRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *Hosts
func (h *HostsRepo) GetHostAttributeKeys(ctx context.Context, req v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
// TODO(srikanthccv): remove hardcoded metric name and support keys from any system metric
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForHostAttributes
req.AggregateAttribute = "system_cpu_load_average_15m"
if req.Limit == 0 {
req.Limit = 50
}
@@ -91,7 +71,7 @@ func (h *HostsRepo) GetHostAttributeKeys(ctx context.Context, req v3.FilterAttri
func (h *HostsRepo) GetHostAttributeValues(ctx context.Context, req v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForHostAttributes
req.AggregateAttribute = "system_cpu_load_average_15m"
if req.Limit == 0 {
req.Limit = 50
}
@@ -100,21 +80,21 @@ func (h *HostsRepo) GetHostAttributeValues(ctx context.Context, req v3.FilterAtt
if err != nil {
return nil, err
}
if req.FilterAttributeKey != hostNameAttrKey {
if req.FilterAttributeKey != "host_name" {
return attributeValuesResponse, nil
}
hostNames := []string{}
for _, attributeValue := range attributeValuesResponse.StringAttributeValues {
if strings.Contains(attributeValue, agentNameToIgnore) {
if strings.Contains(attributeValue, "k8s-infra-otel-agent") {
continue
}
hostNames = append(hostNames, attributeValue)
}
req.FilterAttributeKey = k8sNodeNameAttrKey
req.FilterAttributeKey = "k8s_node_name"
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForHostAttributes
req.AggregateAttribute = "system_cpu_load_average_15m"
if req.Limit == 0 {
req.Limit = 50
}
@@ -124,7 +104,7 @@ func (h *HostsRepo) GetHostAttributeValues(ctx context.Context, req v3.FilterAtt
return nil, err
}
for _, attributeValue := range attributeValuesResponse.StringAttributeValues {
if strings.Contains(attributeValue, agentNameToIgnore) {
if strings.Contains(attributeValue, "k8s-infra-otel-agent") {
continue
}
hostNames = append(hostNames, attributeValue)
@@ -133,6 +113,78 @@ func (h *HostsRepo) GetHostAttributeValues(ctx context.Context, req v3.FilterAtt
return &v3.FilterAttributeValueResponse{StringAttributeValues: hostNames}, nil
}
func getGroupKey(record model.HostListRecord, groupBy []v3.AttributeKey) string {
groupKey := ""
for _, key := range groupBy {
groupKey += fmt.Sprintf("%s=%s,", key.Key, record.Meta[key.Key])
}
return groupKey
}
func (h *HostsRepo) getMetadataAttributes(ctx context.Context,
req model.HostListRequest, hostNameAttrKey string) (map[string]map[string]string, error) {
hostAttrs := map[string]map[string]string{}
hasHostName := false
for _, key := range req.GroupBy {
if key.Key == hostNameAttrKey {
hasHostName = true
}
}
if !hasHostName {
req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: hostNameAttrKey})
}
mq := v3.BuilderQuery{
AggregateAttribute: v3.AttributeKey{
Key: "system_cpu_load_average_15m",
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
GroupBy: req.GroupBy,
}
query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq)
if err != nil {
return nil, err
}
// TODO(srikanthccv): remove this
// What is happening here?
// The `PrepareTimeseriesFilterQuery` uses the local time series table for sub-query because each fingerprint
// goes to same shard.
// However, in this case, we are interested in the attributes values across all the shards.
// So, we replace the local time series table with the distributed time series table.
// See `PrepareTimeseriesFilterQuery` for more details.
query = strings.Replace(query, ".time_series_v4", ".distributed_time_series_v4", 1)
attrsListResponse, err := h.reader.GetListResultV3(ctx, query)
if err != nil {
return nil, err
}
for _, row := range attrsListResponse {
stringData := map[string]string{}
for key, value := range row.Data {
if str, ok := value.(string); ok {
stringData[key] = str
} else if strPtr, ok := value.(*string); ok {
stringData[key] = *strPtr
}
}
hostName := stringData[hostNameAttrKey]
if _, ok := hostAttrs[hostName]; !ok {
hostAttrs[hostName] = map[string]string{}
}
for _, key := range req.GroupBy {
hostAttrs[hostName][key.Key] = stringData[key.Key]
}
}
return hostAttrs, nil
}
func (h *HostsRepo) getActiveHosts(ctx context.Context,
req model.HostListRequest, hostNameAttrKey string) (map[string]bool, error) {
activeStatus := map[string]bool{}
@@ -150,7 +202,7 @@ func (h *HostsRepo) getActiveHosts(ctx context.Context,
}
params := v3.QueryRangeParamsV3{
Start: time.Now().Add(-time.Minute * 10).UTC().UnixMilli(),
Start: time.Now().Add(-time.Hour).UTC().UnixMilli(),
End: time.Now().UTC().UnixMilli(),
Step: step,
CompositeQuery: &v3.CompositeQuery{
@@ -160,7 +212,7 @@ func (h *HostsRepo) getActiveHosts(ctx context.Context,
StepInterval: step,
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricToUseForHostAttributes,
Key: "system_cpu_load_average_15m",
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
@@ -192,103 +244,25 @@ func (h *HostsRepo) getActiveHosts(ctx context.Context,
return activeStatus, nil
}
// getTopHosts returns the top hosts for the given order by column name
func (h *HostsRepo) getTopHosts(ctx context.Context, req model.HostListRequest, q *v3.QueryRangeParamsV3, hostNameAttrKey string) ([]string, []string, error) {
step, timeSeriesTableName, samplesTableName := getParamsForTopHosts(req)
queryNames := queryNamesForTopHosts[req.OrderBy.ColumnName]
topHostsQueryRangeParams := &v3.QueryRangeParamsV3{
Start: req.Start,
End: req.End,
Step: step,
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{},
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
},
}
for _, queryName := range queryNames {
query := q.CompositeQuery.BuilderQueries[queryName].Clone()
query.StepInterval = step
query.MetricTableHints = &v3.MetricTableHints{
TimeSeriesTableName: timeSeriesTableName,
SamplesTableName: samplesTableName,
}
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
topHostsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
}
queryResponse, _, err := h.querierV2.QueryRange(ctx, topHostsQueryRangeParams)
if err != nil {
return nil, nil, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topHostsQueryRangeParams)
if err != nil {
return nil, nil, err
}
if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 {
return nil, nil, nil
}
if req.OrderBy.Order == v3.DirectionDesc {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value
})
} else {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value
})
}
paginatedTopHostsSeries := formattedResponse[0].Series[req.Offset : req.Offset+req.Limit]
topHosts := []string{}
for _, series := range paginatedTopHostsSeries {
topHosts = append(topHosts, series.Labels[hostNameAttrKey])
}
allHosts := []string{}
for _, series := range formattedResponse[0].Series {
allHosts = append(allHosts, series.Labels[hostNameAttrKey])
}
return topHosts, allHosts, nil
}
func (h *HostsRepo) getHostsForQuery(ctx context.Context,
req model.HostListRequest, q *v3.QueryRangeParamsV3, hostNameAttrKey string) ([]model.HostListRecord, []string, error) {
req model.HostListRequest, q *v3.QueryRangeParamsV3, hostNameAttrKey string) ([]model.HostListRecord, error) {
step := common.MinAllowedStepInterval(req.Start, req.End)
query := q.Clone()
if req.OrderBy != nil {
for _, q := range query.CompositeQuery.BuilderQueries {
q.OrderBy = []v3.OrderBy{*req.OrderBy}
}
}
query.Start = req.Start
query.End = req.End
query.Step = step
topHosts, allHosts, err := h.getTopHosts(ctx, req, q, hostNameAttrKey)
if err != nil {
return nil, nil, err
}
for _, query := range query.CompositeQuery.BuilderQueries {
query.StepInterval = step
// check if the filter has host_name and is either IN or EQUAL operator
// if so, we don't need to add the topHosts filter again
hasHostNameInOrEqual := false
if req.Filters != nil && len(req.Filters.Items) > 0 {
for _, item := range req.Filters.Items {
if item.Key.Key == hostNameAttrKey && (item.Operator == v3.FilterOperatorIn || item.Operator == v3.FilterOperatorEqual) {
hasHostNameInOrEqual = true
}
}
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
@@ -296,36 +270,29 @@ func (h *HostsRepo) getHostsForQuery(ctx context.Context,
// what is happening here?
// if the filter has host_name and we are querying for k8s host metrics,
// we need to replace the host_name with k8s_node_name
if hostNameAttrKey == k8sNodeNameAttrKey {
if hostNameAttrKey == "k8s_node_name" {
for idx, item := range query.Filters.Items {
if item.Key.Key == hostNameAttrKey {
query.Filters.Items[idx].Key.Key = k8sNodeNameAttrKey
if item.Key.Key == "host_name" {
query.Filters.Items[idx].Key.Key = "k8s_node_name"
}
}
}
}
if !hasHostNameInOrEqual {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
Key: v3.AttributeKey{
Key: hostNameAttrKey,
},
Value: topHosts,
Operator: v3.FilterOperatorIn,
})
}
}
hostAttrs, err := h.getMetadataAttributes(ctx, req, hostNameAttrKey)
if err != nil {
return nil, err
}
activeHosts, err := h.getActiveHosts(ctx, req, hostNameAttrKey)
if err != nil {
return nil, nil, err
return nil, err
}
queryResponse, _, err := h.querierV2.QueryRange(ctx, query)
if err != nil {
return nil, nil, err
return nil, err
}
type hostTSInfo struct {
@@ -354,7 +321,7 @@ func (h *HostsRepo) getHostsForQuery(ctx context.Context,
formulaResult, err := postprocess.PostProcessResult(queryResponse, query)
if err != nil {
return nil, nil, err
return nil, err
}
for _, result := range formulaResult {
@@ -416,6 +383,7 @@ func (h *HostsRepo) getHostsForQuery(ctx context.Context,
if ok {
record.Load15 = load15
}
record.Meta = hostAttrs[record.HostName]
record.Active = activeHosts[record.HostName]
if hostTSInfoMap[record.HostName] != nil {
record.CPUTimeSeries = hostTSInfoMap[record.HostName].cpuTimeSeries
@@ -426,7 +394,14 @@ func (h *HostsRepo) getHostsForQuery(ctx context.Context,
records = append(records, record)
}
return records, allHosts, nil
if req.Offset > 0 {
records = records[req.Offset:]
}
if req.Limit > 0 && len(records) > req.Limit {
records = records[:req.Limit]
}
return records, nil
}
func dedupRecords(records []model.HostListRecord) []model.HostListRecord {
@@ -442,44 +417,96 @@ func dedupRecords(records []model.HostListRecord) []model.HostListRecord {
}
func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest) (model.HostListResponse, error) {
if req.Limit == 0 {
req.Limit = 10
}
if req.OrderBy == nil {
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
}
resp := model.HostListResponse{
Type: "list",
}
vmRecords, vmAllHosts, err := h.getHostsForQuery(ctx, req, &NonK8STableListQuery, hostNameAttrKey)
vmRecords, err := h.getHostsForQuery(ctx, req, &NonK8STableListQuery, "host_name")
if err != nil {
return resp, err
}
k8sRecords, k8sAllHosts, err := h.getHostsForQuery(ctx, req, &K8STableListQuery, k8sNodeNameAttrKey)
k8sRecords, err := h.getHostsForQuery(ctx, req, &K8STableListQuery, "k8s_node_name")
if err != nil {
return resp, err
}
uniqueHosts := map[string]bool{}
for _, host := range vmAllHosts {
uniqueHosts[host] = true
}
for _, host := range k8sAllHosts {
uniqueHosts[host] = true
}
records := append(vmRecords, k8sRecords...)
// since we added the fix for incorrect host name, it is possible that both host_name and k8s_node_name
// are present in the response. we need to dedup the results.
records = dedupRecords(records)
resp.Total = len(uniqueHosts)
if len(req.GroupBy) > 0 {
groups := []model.HostListGroup{}
groupMap := make(map[string][]model.HostListRecord)
for _, record := range records {
groupKey := getGroupKey(record, req.GroupBy)
if _, ok := groupMap[groupKey]; !ok {
groupMap[groupKey] = []model.HostListRecord{record}
} else {
groupMap[groupKey] = append(groupMap[groupKey], record)
}
}
// calculate the group stats, active hosts, etc.
for _, records := range groupMap {
var avgCPU, avgMemory, avgWait, avgLoad15 float64
var validCPU, validMemory, validWait, validLoad15, activeHosts int
for _, record := range records {
if !math.IsNaN(record.CPU) {
avgCPU += record.CPU
validCPU++
}
if !math.IsNaN(record.Memory) {
avgMemory += record.Memory
validMemory++
}
if !math.IsNaN(record.Wait) {
avgWait += record.Wait
validWait++
}
if !math.IsNaN(record.Load15) {
avgLoad15 += record.Load15
validLoad15++
}
if record.Active {
activeHosts++
}
}
avgCPU /= float64(validCPU)
avgMemory /= float64(validMemory)
avgWait /= float64(validWait)
avgLoad15 /= float64(validLoad15)
inactiveHosts := len(records) - activeHosts
// take any record and make it as the group meta
firstRecord := records[0]
var groupValues []string
for _, key := range req.GroupBy {
groupValues = append(groupValues, firstRecord.Meta[key.Key])
}
hostNames := []string{}
for _, record := range records {
hostNames = append(hostNames, record.HostName)
}
groups = append(groups, model.HostListGroup{
GroupValues: groupValues,
Active: activeHosts,
Inactive: inactiveHosts,
GroupCPUAvg: avgCPU,
GroupMemoryAvg: avgMemory,
GroupWaitAvg: avgWait,
GroupLoad15Avg: avgLoad15,
HostNames: hostNames,
})
}
resp.Groups = groups
resp.Type = "grouped_list"
}
resp.Records = records
resp.Total = len(records)
return resp, nil
}

View File

@@ -1,329 +0,0 @@
package inframetrics
import (
"context"
"math"
"sort"
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
"go.signoz.io/signoz/pkg/query-service/common"
"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/postprocess"
"golang.org/x/exp/slices"
)
var (
metricToUseForNamespaces = "k8s_pod_cpu_utilization"
namespaceAttrsToEnrich = []string{
"k8s_namespace_name",
"k8s_cluster_name",
}
queryNamesForNamespaces = map[string][]string{
"cpu": {"A"},
"memory": {"D"},
}
namespaceQueryNames = []string{"A", "D"}
attributesKeysForNamespaces = []v3.AttributeKey{
{Key: "k8s_namespace_name"},
{Key: "k8s_cluster_name"},
}
k8sNamespaceNameAttrKey = "k8s_namespace_name"
)
type NamespacesRepo struct {
reader interfaces.Reader
querierV2 interfaces.Querier
}
func NewNamespacesRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *NamespacesRepo {
return &NamespacesRepo{reader: reader, querierV2: querierV2}
}
func (p *NamespacesRepo) GetNamespaceAttributeKeys(ctx context.Context, req v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
return &v3.FilterAttributeKeyResponse{AttributeKeys: attributesKeysForNamespaces}, nil
}
func (p *NamespacesRepo) GetNamespaceAttributeValues(ctx context.Context, req v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForNamespaces
if req.Limit == 0 {
req.Limit = 50
}
attributeValuesResponse, err := p.reader.GetMetricAttributeValues(ctx, &req)
if err != nil {
return nil, err
}
return attributeValuesResponse, nil
}
func (p *NamespacesRepo) getMetadataAttributes(ctx context.Context, req model.NamespaceListRequest) (map[string]map[string]string, error) {
namespaceAttrs := map[string]map[string]string{}
for _, key := range namespaceAttrsToEnrich {
hasKey := false
for _, groupByKey := range req.GroupBy {
if groupByKey.Key == key {
hasKey = true
break
}
}
if !hasKey {
req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: key})
}
}
mq := v3.BuilderQuery{
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricToUseForNamespaces,
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
GroupBy: req.GroupBy,
}
query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq)
if err != nil {
return nil, err
}
query = localQueryToDistributedQuery(query)
attrsListResponse, err := p.reader.GetListResultV3(ctx, query)
if err != nil {
return nil, err
}
for _, row := range attrsListResponse {
stringData := map[string]string{}
for key, value := range row.Data {
if str, ok := value.(string); ok {
stringData[key] = str
} else if strPtr, ok := value.(*string); ok {
stringData[key] = *strPtr
}
}
namespaceName := stringData[k8sNamespaceNameAttrKey]
if _, ok := namespaceAttrs[namespaceName]; !ok {
namespaceAttrs[namespaceName] = map[string]string{}
}
for _, key := range req.GroupBy {
namespaceAttrs[namespaceName][key.Key] = stringData[key.Key]
}
}
return namespaceAttrs, nil
}
func (p *NamespacesRepo) getTopNamespaceGroups(ctx context.Context, req model.NamespaceListRequest, q *v3.QueryRangeParamsV3) ([]map[string]string, []map[string]string, error) {
step, timeSeriesTableName, samplesTableName := getParamsForTopNamespaces(req)
queryNames := queryNamesForNamespaces[req.OrderBy.ColumnName]
topNamespaceGroupsQueryRangeParams := &v3.QueryRangeParamsV3{
Start: req.Start,
End: req.End,
Step: step,
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{},
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
},
}
for _, queryName := range queryNames {
query := q.CompositeQuery.BuilderQueries[queryName].Clone()
query.StepInterval = step
query.MetricTableHints = &v3.MetricTableHints{
TimeSeriesTableName: timeSeriesTableName,
SamplesTableName: samplesTableName,
}
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
topNamespaceGroupsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, topNamespaceGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topNamespaceGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 {
return nil, nil, nil
}
if req.OrderBy.Order == v3.DirectionDesc {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value
})
} else {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value
})
}
paginatedTopNamespaceGroupsSeries := formattedResponse[0].Series[req.Offset : req.Offset+req.Limit]
topNamespaceGroups := []map[string]string{}
for _, series := range paginatedTopNamespaceGroupsSeries {
topNamespaceGroups = append(topNamespaceGroups, series.Labels)
}
allNamespaceGroups := []map[string]string{}
for _, series := range formattedResponse[0].Series {
allNamespaceGroups = append(allNamespaceGroups, series.Labels)
}
return topNamespaceGroups, allNamespaceGroups, nil
}
func (p *NamespacesRepo) GetNamespaceList(ctx context.Context, req model.NamespaceListRequest) (model.NamespaceListResponse, error) {
resp := model.NamespaceListResponse{}
if req.Limit == 0 {
req.Limit = 10
}
if req.OrderBy == nil {
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
}
if req.GroupBy == nil {
req.GroupBy = []v3.AttributeKey{{Key: k8sNamespaceNameAttrKey}}
resp.Type = model.ResponseTypeList
} else {
resp.Type = model.ResponseTypeGroupedList
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := PodsTableListQuery.Clone()
query.Start = req.Start
query.End = req.End
query.Step = step
for _, q := range query.CompositeQuery.BuilderQueries {
if !slices.Contains(namespaceQueryNames, q.QueryName) {
delete(query.CompositeQuery.BuilderQueries, q.QueryName)
}
q.StepInterval = step
if req.Filters != nil && len(req.Filters.Items) > 0 {
if q.Filters == nil {
q.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
q.Filters.Items = append(q.Filters.Items, req.Filters.Items...)
}
q.GroupBy = req.GroupBy
}
namespaceAttrs, err := p.getMetadataAttributes(ctx, req)
if err != nil {
return resp, err
}
topNamespaceGroups, allNamespaceGroups, err := p.getTopNamespaceGroups(ctx, req, query)
if err != nil {
return resp, err
}
groupFilters := map[string][]string{}
for _, topNamespaceGroup := range topNamespaceGroups {
for k, v := range topNamespaceGroup {
groupFilters[k] = append(groupFilters[k], v)
}
}
for groupKey, groupValues := range groupFilters {
hasGroupFilter := false
if req.Filters != nil && len(req.Filters.Items) > 0 {
for _, filter := range req.Filters.Items {
if filter.Key.Key == groupKey {
hasGroupFilter = true
break
}
}
}
if !hasGroupFilter {
for _, query := range query.CompositeQuery.BuilderQueries {
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
Key: v3.AttributeKey{Key: groupKey},
Value: groupValues,
Operator: v3.FilterOperatorIn,
})
}
}
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, query)
if err != nil {
return resp, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, query)
if err != nil {
return resp, err
}
records := []model.NamespaceListRecord{}
for _, result := range formattedResponse {
for _, row := range result.Table.Rows {
record := model.NamespaceListRecord{
CPUUsage: -1,
MemoryUsage: -1,
}
if name, ok := row.Data[k8sNamespaceNameAttrKey].(string); ok {
record.NamespaceName = name
}
if cpu, ok := row.Data["A"].(float64); ok {
record.CPUUsage = cpu
}
if memory, ok := row.Data["D"].(float64); ok {
record.MemoryUsage = memory
}
record.Meta = map[string]string{}
if _, ok := namespaceAttrs[record.NamespaceName]; ok {
record.Meta = namespaceAttrs[record.NamespaceName]
}
for k, v := range row.Data {
if slices.Contains(namespaceQueryNames, k) {
continue
}
if labelValue, ok := v.(string); ok {
record.Meta[k] = labelValue
}
}
records = append(records, record)
}
}
resp.Total = len(allNamespaceGroups)
resp.Records = records
return resp, nil
}

Some files were not shown because too many files have changed in this diff Show More