Compare commits

..

19 Commits

Author SHA1 Message Date
Yunus M
bae443904e feat: remove hardcode is darkmode value from get anomaly data func 2024-10-23 19:12:14 +05:30
Yunus M
9419f56e95 feat: tooltip plugin to show series data in tooltip (#6194)
* feat: tooltip plugin to show series data in tooltip

* feat: send anomaly function as last function

* feat: default z_score_threshold to 3

* fix: anomaly function not applied on initial load

* feat: maintain select alert type on reload

* feat: maintain select alert type on reload

* chore: update events to handle anomaly alert interactions
2024-10-23 19:06:38 +05:30
Sai Chander
347868c18b feat: enable offline functionality for frontend (#6152)
Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-10-23 05:53:51 +00:00
Srikanth Chekuri
17e20e7f41 chore: split migrator job to sync and async (#6107) 2024-10-23 00:05:10 +05:30
Nityananda Gohain
2b0da82f94 feat: move resource qb to its own package and use common options (#6238)
* feat: move resource qb to its own package and use common options

* fix: add remaining files

* fix: remove redundant struct
2024-10-22 16:46:58 +05:30
Vikrant Gupta
911362cecf chore: remove the required check from organisation name (#6225) 2024-10-21 18:34:37 +05:30
SagarRajput-7
481f9620d3 chore: added doc link for kafka on getStarted for non-cloud users (#6222)
* chore: added doc link for kafka on getStarted for non-cloud users

* chore: added link with utm parameter
2024-10-21 15:52:35 +05:30
SagarRajput-7
e5be431f18 fix: removed selectedValue & uuid from exported and copied dashboard json (#6145)
* fix: removed selectedValue from exported and copied dashboard json

* fix: added uuid validation to not expect empty uuid

* fix: removed uuid from export and copied dashboard json

* fix: resolved comment and removed uuid when empty

* fix: renamed function
2024-10-21 15:51:43 +05:30
SagarRajput-7
503ed45a99 fix: fixed threshold for columns with units (#6079)
* fix: fixed threshold for columns with units

* fix: added interunit and category conversion for handling threshold across different unit types

* fix: added invalid comparison text and removed unsupported unit categories

* fix: cleanup and multiple threshold and state change handling

* fix: restrict category select to only columnUnit group

* fix: restricted column name from threshold option

* fix: removed console log

* fix: resolved comments and some refactoring

* fix: resolved comments and some refactoring
2024-10-21 15:51:22 +05:30
Srikanth Chekuri
28818fbaac chore: update query builder to use 5min/30min aggregation tables (#5679) 2024-10-21 14:22:32 +05:30
Vikrant Gupta
c0e40614bf fix: issues with the logs where clause filter (#6198)
* fix: flicker in logs explorer page

* fix: added inline code comments

* fix: edit rules css should be scoped inside the container

* fix: add explicit space after adding key and operators to help users type and create the tag

* fix: do not wait for api to complete for the user to enter the query

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-10-21 11:42:20 +05:30
Akira Hayashi
2d732ae4a9 chore: run all query-service tests (#6098) 2024-10-20 15:24:12 +00:00
Vikrant Gupta
8466e31e02 chore: change the log background to same as severity text with some opacity (#6217)
* chore: change the log background to same as severity text with some opacity

* chore: make the log background changes for column renderer

* chore: remove the backdrop mask from logs details drawer

* chore: fix tests
2024-10-18 18:19:42 +05:30
Vikrant Gupta
efdaf7ee43 chore: make the clear selection view more highlighted (#6216)
* chore: make the clear selection view more highlighted

* chore: address minor design issue
2024-10-18 18:18:30 +05:30
Vikrant Gupta
0dec94a5c6 chore: remove the trial expiry banner on logout (#6212) 2024-10-18 18:17:17 +05:30
Shivanshu Raj Shrivastava
204728ff60 chore: move constants to fix ci (#6213) 2024-10-18 01:15:54 +05:30
Shivanshu Raj Shrivastava
e51f4d986d feat: kafka Scenario 1, 3, 4 all squashed (#6144)
* feat: kafka partition level observerability features
2024-10-17 19:44:42 +05:30
Vishal Sharma
337a941d0d feat: onboarding API via proxy (#6058)
* feat: onboarding API via proxy

* chore: update profiles route

* chore: update profiles url
2024-10-17 19:09:10 +05:30
Vishal Sharma
fc4b55cb34 feat: bulk invite user api (#6057) 2024-10-17 18:41:31 +05:30
136 changed files with 3376 additions and 3688 deletions

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,13 +188,4 @@ check-no-ee-references:
fi
test:
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/...
go test ./pkg/query-service/...

View File

@@ -34,7 +34,7 @@ x-db-depend: &db-depend
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator:
otel-collector-migrator-sync:
condition: service_completed_successfully
# clickhouse-2:
# condition: service_healthy
@@ -212,11 +212,13 @@ services:
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
otel-collector-migrator-sync:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
container_name: otel-migrator
container_name: otel-migrator-sync
command:
- "sync"
- "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on:
clickhouse:
condition: service_healthy
@@ -225,6 +227,22 @@ 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}
@@ -262,7 +280,7 @@ services:
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator:
otel-collector-migrator-sync:
condition: service_completed_successfully
query-service:
condition: service_healthy

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

1
frontend/public/css/uPlot.min.css vendored Normal file
View File

@@ -0,0 +1 @@
.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;}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -224,10 +224,3 @@ export const MQDetailPage = Loadable(
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
),
);
export const InfrastructureMonitoring = Loadable(
() =>
import(
/* webpackChunkName: "InfrastructureMonitoring" */ 'pages/InfrastructureMonitoring'
),
);

View File

@@ -15,7 +15,6 @@ import {
EditAlertChannelsAlerts,
EditRulesPage,
ErrorDetails,
InfrastructureMonitoring,
IngestionSettings,
InstalledIntegrations,
LicensePage,
@@ -384,13 +383,6 @@ const routes: AppRoutes[] = [
key: 'MESSAGING_QUEUES_DETAIL',
isPrivate: true,
},
{
path: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
exact: true,
component: InfrastructureMonitoring,
key: 'INFRASTRUCTURE_MONITORING_HOSTS',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -1,75 +0,0 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface HostListPayload {
filters: TagFilter;
groupBy: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface TimeSeriesValue {
timestamp: number;
value: string;
}
export interface TimeSeries {
labels: Record<string, string>;
labelsArray: Array<Record<string, string>>;
values: TimeSeriesValue[];
}
export interface HostData {
hostName: string;
active: boolean;
os: string;
cpu: number;
cpuTimeSeries: TimeSeries;
memory: number;
memoryTimeSeries: TimeSeries;
wait: number;
waitTimeSeries: TimeSeries;
load15: number;
load15TimeSeries: TimeSeries;
}
export interface HostListResponse {
status: string;
data: {
type: string;
records: HostData[];
groups: null;
total: number;
};
}
export const getHostLists = async (
props: HostListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<HostListResponse> | ErrorResponse> => {
try {
const response = await ApiBaseInstance.post('/hosts/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,38 +0,0 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IAttributeValuesResponse,
IGetAttributeValuesPayload,
} from 'types/api/queryBuilder/getAttributesValues';
export const getInfraAttributesValues = async ({
dataSource,
attributeKey,
filterAttributeKeyDataType,
tagType,
searchText,
}: IGetAttributeValuesPayload): Promise<
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
> => {
try {
const response = await ApiBaseInstance.get(
`/hosts/attribute_values?${createQueryParams({
dataSource,
attributeKey,
searchText,
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,4 +1,4 @@
import { ApiBaseInstance, ApiV3Instance } from 'api';
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
@@ -18,25 +18,20 @@ export const getAggregateKeys = async ({
dataSource,
aggregateAttribute,
tagType,
isInfraMonitoring,
}: IGetAttributeKeysPayload): Promise<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
> => {
try {
const endpoint = isInfraMonitoring
? `/hosts/attribute_keys?dataSource=metrics&searchText=${searchText || ''}`
: `/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator,
searchText,
dataSource,
aggregateAttribute,
})}&tagType=${tagType}`;
const apiInstance = isInfraMonitoring ? ApiBaseInstance : ApiV3Instance;
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await apiInstance.get(endpoint);
}> = await ApiV3Instance.get(
`/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator,
searchText,
dataSource,
aggregateAttribute,
})}&tagType=${tagType}`,
);
const payload: BaseAutocompleteData[] =
response.data.data.attributeKeys?.map(({ id: _, ...item }) => ({

View File

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

View File

@@ -195,21 +195,20 @@ function ListLogView({
return (
<>
<Container
$isActiveLog={isHighlighted}
$isActiveLog={
isHighlighted ||
activeLog?.id === logData.id ||
activeContextLog?.id === logData.id
}
$isDarkMode={isDarkMode}
$logType={logType}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleDetailedView}
fontSize={fontSize}
>
<div className="log-line">
<LogStateIndicator
type={logType}
isActive={
activeLog?.id === logData.id || activeContextLog?.id === logData.id
}
fontSize={fontSize}
/>
<LogStateIndicator type={logType} 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,6 +15,7 @@ interface LogContainerProps {
export const Container = styled(Card)<{
$isActiveLog: boolean;
$isDarkMode: boolean;
$logType: string;
fontSize: FontSize;
}>`
width: 100% !important;
@@ -41,13 +42,8 @@ export const Container = styled(Card)<{
? `padding:0.3rem 0.6rem;`
: ``}
${({ $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
: ''}
}
${({ $isActiveLog, $isDarkMode, $logType }): string =>
getActiveLogBackground($isActiveLog, $isDarkMode, $logType)}
`;
export const Text = styled(Typography.Text)`

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,8 +35,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
linesPerRow,
fontSize,
appendTo = 'center',
activeContextLog,
activeLog,
isListViewPanel,
} = props;
@@ -90,9 +88,6 @@ 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)}>
@@ -130,16 +125,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
},
...(appendTo === 'end' ? fieldColumns : []),
];
}, [
fields,
isListViewPanel,
appendTo,
isDarkMode,
linesPerRow,
activeLog?.id,
activeContextLog?.id,
fontSize,
]);
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
return { columns, dataSource: flattenLogData };
};

View File

@@ -3,10 +3,6 @@ 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

@@ -18,5 +18,4 @@ export const REACT_QUERY_KEY = {
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
};

View File

@@ -58,7 +58,6 @@ const ROUTES = {
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
} as const;
export default ROUTES;

View File

@@ -63,6 +63,16 @@
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;
}
@@ -108,3 +118,63 @@
}
}
}
.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

@@ -13,6 +13,8 @@ import { LineChart } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import uPlot from 'uplot';
import tooltipPlugin from './tooltipPlugin';
function UplotChart({
data,
options,
@@ -72,13 +74,13 @@ function AnomalyAlertEvaluationView({
const dimensions = useResizeObserver(graphRef);
useEffect(() => {
const chartData = getUplotChartDataForAnomalyDetection(data);
const chartData = getUplotChartDataForAnomalyDetection(data, isDarkMode);
setSeriesData(chartData);
setAllSeries(Object.keys(chartData));
setFilteredSeriesKeys(Object.keys(chartData));
}, [data]);
}, [data, isDarkMode]);
useEffect(() => {
const seriesKeys = Object.keys(seriesData);
@@ -149,10 +151,38 @@ function AnomalyAlertEvaluationView({
const options = {
width: dimensions.width,
height: dimensions.height - 36,
plugins: [bandsPlugin],
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
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',
@@ -165,6 +195,7 @@ function AnomalyAlertEvaluationView({
width: 2,
show: true,
paths: _spline,
spanGaps: true,
},
{
label: `Predicted Value`,
@@ -173,18 +204,29 @@ function AnomalyAlertEvaluationView({
dash: [2, 2],
show: true,
paths: _spline,
spanGaps: true,
},
{
label: `Upper Band`,
stroke: 'transparent',
show: false,
show: true,
paths: _spline,
spanGaps: true,
points: {
show: false,
size: 1,
},
},
{
label: `Lower Band`,
stroke: 'transparent',
show: false,
show: true,
paths: _spline,
spanGaps: true,
points: {
show: false,
size: 1,
},
},
]
: allSeries.map((seriesKey) => ({
@@ -193,11 +235,13 @@ function AnomalyAlertEvaluationView({
width: 2,
show: true,
paths: _spline,
spanGaps: true,
}))),
],
scales: {
x: {
time: true,
spanGaps: true,
},
y: {
...getYAxisScaleForAnomalyDetection({
@@ -211,9 +255,6 @@ function AnomalyAlertEvaluationView({
grid: {
show: true,
},
legend: {
show: true,
},
axes: getAxes(isDarkMode, yAxisUnit),
};
@@ -287,17 +328,24 @@ function AnomalyAlertEvaluationView({
)}
{filteredSeriesKeys.map((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)}
>
{seriesKey}
</Checkbox>
<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 && (

View File

@@ -0,0 +1,148 @@
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,6 +211,13 @@ 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,6 +4,7 @@ import {
initialQueryPromQLData,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
AlertDef,
@@ -58,6 +59,49 @@ 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: {
@@ -149,7 +193,7 @@ export const exceptionAlertDefaults: AlertDef = {
};
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
[AlertTypes.ANOMALY_BASED_ALERT]: alertDefaults,
[AlertTypes.ANOMALY_BASED_ALERT]: anamolyAlertDefaults,
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,

View File

@@ -13,6 +13,7 @@ import { AlertDef } from 'types/api/alerts/def';
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
import {
alertDefaults,
anamolyAlertDefaults,
exceptionAlertDefaults,
logAlertDefaults,
traceAlertDefaults,
@@ -24,8 +25,12 @@ 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 = queryParams.get(QueryParams.alertType);
const alertTypeFromParams =
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
? AlertTypes.ANOMALY_BASED_ALERT
: queryParams.get(QueryParams.alertType);
const compositeQuery = useGetCompositeQueryParam();
function getAlertTypeFromDataSource(): AlertTypes | null {
@@ -58,7 +63,7 @@ function CreateRules(): JSX.Element {
break;
case AlertTypes.ANOMALY_BASED_ALERT:
setInitValues({
...alertDefaults,
...anamolyAlertDefaults,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
});
@@ -78,7 +83,10 @@ function CreateRules(): JSX.Element {
: typ,
);
if (typ === AlertTypes.ANOMALY_BASED_ALERT) {
if (
typ === AlertTypes.ANOMALY_BASED_ALERT ||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_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 12px;
padding: 10px 10px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6);
@@ -33,6 +33,7 @@
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
cursor: pointer;
box-shadow: none;
}
.hidden {

View File

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

View File

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

View File

@@ -160,6 +160,15 @@ 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}
@@ -203,6 +212,28 @@ 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}
@@ -237,39 +268,6 @@ 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>
@@ -320,6 +318,32 @@ 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}
@@ -354,7 +378,7 @@ function RuleOptions({
{queryCategory === EQueryType.PROM && renderPromRuleOptions()}
{queryCategory !== EQueryType.PROM &&
ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
<>{renderAnomalyRuleOpts(onChange)}</>
<>{renderAnomalyRuleOpts()}</>
)}
{queryCategory !== EQueryType.PROM &&

View File

@@ -39,6 +39,7 @@ 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 {
@@ -88,6 +89,8 @@ 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;
@@ -120,9 +123,7 @@ function FormAlertRules({
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
const [detectionMethod, setDetectionMethod] = useState<string>(
AlertDetectionTypes.THRESHOLD_ALERT,
);
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
useEffect(() => {
if (!isEqual(currentQuery.unit, yAxisUnit)) {
@@ -154,11 +155,24 @@ 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: 9 },
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
};
const functions = data.functions || [];
@@ -166,6 +180,19 @@ 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
@@ -178,6 +205,20 @@ 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];
@@ -191,7 +232,11 @@ function FormAlertRules({
useEffect(() => {
updateFunctionsBasedOnAlertType();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [detectionMethod, alertDef, currentQuery.builder.queryData.length]);
}, [
detectionMethod,
alertDef.condition.target,
currentQuery.builder.queryData.length,
]);
useEffect(() => {
const broadcastToSpecificChannels =
@@ -215,7 +260,8 @@ function FormAlertRules({
});
setDetectionMethod(ruleType);
}, [initialValue, isNewRule, alertTypeFromURL]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValue, isNewRule]);
useEffect(() => {
// Set selectedQueryName based on the length of queryOptions
@@ -335,7 +381,11 @@ function FormAlertRules({
return false;
}
if (alertDef.condition?.target !== 0 && !alertDef.condition?.target) {
if (
alertDef.ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT &&
alertDef.condition?.target !== 0 &&
!alertDef.condition?.target
) {
notifications.error({
message: 'Error',
description: t('target_missing'),
@@ -493,6 +543,7 @@ function FormAlertRules({
queryType: currentQuery.queryType,
alertId: postableAlert?.id,
alertName: postableAlert?.alert,
ruleType: postableAlert?.ruleType,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
@@ -577,6 +628,7 @@ 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]);
@@ -672,15 +724,6 @@ function FormAlertRules({
},
];
const handleDetectionMethodChange = (value: any): void => {
setAlertDef((def) => ({
...def,
ruleType: value,
}));
setDetectionMethod(value);
};
const isAnomalyDetectionEnabled =
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
@@ -745,7 +788,7 @@ function FormAlertRules({
<Tabs2
key={detectionMethod}
tabs={tabs}
initialSelectedTab={detectionMethod}
initialSelectedTab={detectionMethod || ''}
onSelectTab={handleDetectionMethodChange}
/>

View File

@@ -97,13 +97,19 @@ function GridTableComponent({
const newColumnData = columns.map((e) => ({
...e,
render: (text: string): ReactNode => {
const isNumber = !Number.isNaN(Number(text));
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));
if (thresholds && isNumber) {
const { hasMultipleMatches, threshold } = findMatchingThreshold(
thresholds,
e.title as string,
Number(text),
Number(textForThreshold),
columnUnits?.[e.title as string],
);
const idx = thresholds.findIndex(

View File

@@ -1,5 +1,6 @@
/* 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';
@@ -30,10 +31,39 @@ 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;
@@ -45,10 +75,12 @@ export function findMatchingThreshold(
if (
threshold.thresholdValue !== undefined &&
threshold.thresholdTableOptions === label &&
evaluateCondition(
threshold.thresholdOperator,
evaluateThresholdWithConvertedValue(
value,
threshold.thresholdValue,
threshold?.thresholdValue,
threshold.thresholdOperator,
threshold.thresholdUnit,
columnUnit,
)
) {
matchingThresholds.push(threshold);

View File

@@ -1,19 +0,0 @@
.loading-host-metrics {
padding: 24px 0;
height: 600px;
display: flex;
justify-content: center;
align-items: center;
.loading-host-metrics-content {
display: flex;
align-items: center;
flex-direction: column;
.loading-gif {
height: 72px;
margin-left: -24px;
}
}
}

View File

@@ -1,26 +0,0 @@
import './HostMetricsLoading.styles.scss';
import { Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import { DataSource } from 'types/common/queryBuilder';
export function HostMetricsLoading(): JSX.Element {
const { t } = useTranslation('common');
return (
<div className="loading-host-metrics">
<div className="loading-host-metrics-content">
<img
className="loading-gif"
src="/Icons/loading-plane.gif"
alt="wait-icon"
/>
<Typography>
{t('pending_data_placeholder', {
dataSource: `host ${DataSource.METRICS}`,
})}
</Typography>
</div>
</div>
);
}

View File

@@ -82,6 +82,12 @@ 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 {
@@ -123,11 +129,14 @@ function ImportJSON({
});
}
setDashboardCreating(false);
} catch {
} catch (error) {
setDashboardCreating(false);
setIsFeatureAlert(false);
setIsCreateDashboardError(true);
notifications.error({
message: error instanceof Error ? error.message : t('error_loading_json'),
});
}
};

View File

@@ -2,3 +2,25 @@
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,5 +1,6 @@
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';
@@ -21,6 +22,11 @@ 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,
@@ -31,10 +37,17 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
const isDarkMode = useIsDarkMode();
const logType = getLogIndicatorType(props.item);
return (
<TableRowStyled
$isDarkMode={isDarkMode}
$isActiveLog={isHighlighted}
$isActiveLog={
isHighlighted ||
(context as CustomTableRowProps).activeContextLogId === props.item.id ||
(context as CustomTableRowProps).activeLogId === props.item.id
}
$logType={logType}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
@@ -66,8 +79,6 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
...tableViewProps,
onClickExpand: onSetActiveLog,
onOpenLogsContext: handleSetActiveContextLog,
activeLog,
activeContextLog,
});
const { draggedColumns, onDragColumns } = useDragColumns<
@@ -153,7 +164,14 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
// TODO: fix it in the future
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
TableRow: CustomTableRow,
TableRow: (props): any =>
CustomTableRow({
...props,
context: {
activeContextLogId: activeContextLog?.id,
activeLogId: activeLog?.id,
},
} as any),
}}
itemContent={itemContent}
fixedHeaderContent={tableHeader}

View File

@@ -1,5 +1,4 @@
/* 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';
@@ -37,13 +36,12 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
export const TableRowStyled = styled.tr<{
$isActiveLog: boolean;
$isDarkMode: boolean;
$logType: string;
}>`
td {
${({ $isActiveLog, $isDarkMode }): string =>
${({ $isActiveLog, $isDarkMode, $logType }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
: ''};
}

View File

@@ -47,7 +47,11 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
import {
Dashboard,
DashboardData,
IDashboardVariable,
} from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -63,6 +67,30 @@ 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;
@@ -407,7 +435,10 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
type="text"
icon={<FileJson size={14} />}
onClick={(): void => {
downloadObjectAsJson(selectedData, selectedData.title);
downloadObjectAsJson(
sanitizeDashboardData(selectedData),
selectedData.title,
);
setIsDashbordSettingsOpen(false);
}}
>
@@ -417,7 +448,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
type="text"
icon={<ClipboardCopy size={14} />}
onClick={(): void => {
setCopy(JSON.stringify(selectedData, null, 2));
setCopy(
JSON.stringify(sanitizeDashboardData(selectedData), null, 2),
);
setIsDashbordSettingsOpen(false);
}}
>

View File

@@ -297,6 +297,16 @@
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,17 +3,18 @@ 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 { useRef, useState } from 'react';
import { useMemo, 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';
@@ -40,6 +41,7 @@ function Threshold({
thresholdLabel = '',
tableOptions,
thresholdTableOptions = '',
columnUnits,
}: ThresholdProps): JSX.Element {
const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled);
const [operator, setOperator] = useState<string | number>(
@@ -192,6 +194,13 @@ 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}
@@ -303,7 +312,7 @@ function Threshold({
{isEditMode ? (
<Select
defaultValue={unit}
options={unitOptions}
options={unitOptions(columnUnits?.[tableSelectedOption] || '')}
onChange={handleUnitChange}
showSearch
className="unit-selection"
@@ -339,6 +348,12 @@ 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,14 +3,12 @@
import './ThresholdSelector.styles.scss';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { Events } from 'constants/events';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Antenna, Plus } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { eventEmitter } from 'utils/getEventEmitter';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import Threshold from './Threshold';
@@ -21,22 +19,23 @@ function ThresholdSelector({
setThresholds,
yAxisUnit,
selectedGraph,
columnUnits,
}: ThresholdSelectorProps): JSX.Element {
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 { 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 moveThreshold = useCallback(
(dragIndex: number, hoverIndex: number) => {
@@ -66,7 +65,7 @@ function ThresholdSelector({
moveThreshold,
keyIndex: thresholds.length,
selectedGraph,
thresholdTableOptions: tableOptions[0]?.value || '',
thresholdTableOptions: aggregationQueries[0] || '',
},
...thresholds,
]);
@@ -105,8 +104,12 @@ function ThresholdSelector({
moveThreshold={moveThreshold}
selectedGraph={selectedGraph}
thresholdLabel={threshold.thresholdLabel}
tableOptions={tableOptions}
tableOptions={aggregationQueries.map((query) => ({
value: query,
label: query,
}))}
thresholdTableOptions={threshold.thresholdTableOptions}
columnUnits={columnUnits}
/>
))}
</div>

View File

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

View File

@@ -1,8 +1,5 @@
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: '>' },
@@ -11,11 +8,6 @@ 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,3 +438,168 @@ 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,6 +311,7 @@ function RightContainer({
setThresholds={setThresholds}
yAxisUnit={yAxisUnit}
selectedGraph={selectedGraph}
columnUnits={columnUnits}
/>
</section>
)}

View File

@@ -1,3 +1,4 @@
import { DefaultOptionType } from 'antd/es/select';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import {
initialQueryBuilderFormValuesMap,
@@ -8,12 +9,19 @@ import {
listViewInitialTraceQuery,
PANEL_TYPES_INITIAL_QUERY,
} from 'container/NewDashboard/ComponentsSlider/constants';
import { cloneDeep, isEqual, set, unset } from 'lodash-es';
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
import { cloneDeep, isEmpty, 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,
@@ -529,3 +537,41 @@ 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

@@ -13,7 +13,7 @@ import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
interface FunctionProps {
query: IBuilderQuery;
@@ -57,6 +57,13 @@ 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
@@ -92,6 +99,7 @@ export default function Function({
<Button
className="periscope-btn query-function-delete-btn"
disabled={disableRemoveFunction}
onClick={(): void => {
handleDeleteFunction(funcData, index);
}}

View File

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

View File

@@ -4,6 +4,5 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type GroupByFilterProps = {
query: IBuilderQuery;
onChange: (values: BaseAutocompleteData[]) => void;
disabled?: boolean;
isInfraMonitoring?: boolean;
disabled: boolean;
};

View File

@@ -25,7 +25,6 @@ export const GroupByFilter = memo(function GroupByFilter({
query,
onChange,
disabled,
isInfraMonitoring,
}: GroupByFilterProps): JSX.Element {
const queryClient = useQueryClient();
const [searchText, setSearchText] = useState<string>('');
@@ -86,7 +85,6 @@ export const GroupByFilter = memo(function GroupByFilter({
setOptionsData(options);
},
},
isInfraMonitoring,
);
const getAttributeKeys = useCallback(async () => {
@@ -98,7 +96,6 @@ export const GroupByFilter = memo(function GroupByFilter({
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText,
isInfraMonitoring,
}),
);
@@ -110,7 +107,6 @@ export const GroupByFilter = memo(function GroupByFilter({
query.dataSource,
queryClient,
searchText,
isInfraMonitoring,
]);
const handleSearchKeys = (searchText: string): void => {

View File

@@ -72,7 +72,6 @@ function QueryBuilderSearch({
className,
placeholder,
suffixIcon,
isInfraMonitoring,
}: QueryBuilderSearchProps): JSX.Element {
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
@@ -94,12 +93,7 @@ function QueryBuilderSearch({
searchKey,
key,
exampleQueries,
} = useAutoComplete(
query,
whereClauseConfig,
isLogsExplorerPage,
isInfraMonitoring,
);
} = useAutoComplete(query, whereClauseConfig, isLogsExplorerPage);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [showAllFilters, setShowAllFilters] = useState<boolean>(false);
const [dynamicPlacholder, setDynamicPlaceholder] = useState<string>(
@@ -111,7 +105,6 @@ function QueryBuilderSearch({
query,
searchKey,
isLogsExplorerPage,
isInfraMonitoring,
);
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
@@ -192,8 +185,8 @@ function QueryBuilderSearch({
);
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS && !isInfraMonitoring,
[query.dataSource, isInfraMonitoring],
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
const fetchValueDataType = (value: unknown, operator: string): DataTypes => {
@@ -433,7 +426,6 @@ interface QueryBuilderSearchProps {
className?: string;
placeholder?: string;
suffixIcon?: React.ReactNode;
isInfraMonitoring?: boolean;
}
QueryBuilderSearch.defaultProps = {
@@ -441,7 +433,6 @@ QueryBuilderSearch.defaultProps = {
className: '',
placeholder: PLACEHOLDER,
suffixIcon: undefined,
isInfraMonitoring: false,
};
export interface CustomTagProps {

View File

@@ -1,4 +1,6 @@
/* 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,11 +512,6 @@ 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);
@@ -766,6 +761,7 @@ 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',
@@ -788,16 +784,14 @@ 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

@@ -11,7 +11,6 @@ import {
LayoutGrid,
ListMinus,
MessageSquare,
PackagePlus,
Receipt,
Route,
ScrollText,
@@ -119,11 +118,6 @@ const menuItems: SidebarItem[] = [
label: 'Billing',
icon: <Receipt size={16} />,
},
{
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
label: 'Infrastructure Monitoring',
icon: <PackagePlus size={16} />,
},
{
key: ROUTES.SETTINGS,
label: 'Settings',

View File

@@ -212,7 +212,6 @@ export const routesToSkip = [
ROUTES.ALERT_OVERVIEW,
ROUTES.MESSAGING_QUEUES,
ROUTES.MESSAGING_QUEUES_DETAIL,
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@@ -1,34 +0,0 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
type UseGetAttributeKeys = (
requestData: IGetAttributeKeysPayload,
options?: UseQueryOptions<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>,
) => UseQueryResult<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>;
export const useGetAggregateKeys: UseGetAttributeKeys = (
requestData,
options,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, ...options.queryKey];
}
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({
queryKey,
queryFn: () => getAggregateKeys(requestData),
...options,
});
};

View File

@@ -1,42 +0,0 @@
import {
getHostLists,
HostListPayload,
HostListResponse,
} from 'api/infraMonitoring/getHostLists';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetHostList = (
requestData: HostListPayload,
options?: UseQueryOptions<
SuccessResponse<HostListResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<SuccessResponse<HostListResponse> | ErrorResponse, Error>;
export const useGetHostList: UseGetHostList = (
requestData,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_HOST_LIST, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<HostListResponse> | ErrorResponse, Error>({
queryFn: ({ signal }) => getHostLists(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -28,7 +28,6 @@ export const useAutoComplete = (
query: IBuilderQuery,
whereClauseConfig?: WhereClauseConfig,
shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,
): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>('');
const [searchKey, setSearchKey] = useState<string>('');
@@ -38,7 +37,6 @@ export const useAutoComplete = (
query,
searchKey,
shouldUseSuggestions,
isInfraMonitoring,
);
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
@@ -172,5 +170,4 @@ interface IAutoComplete {
searchKey: string;
key: string;
exampleQueries: TagFilter[];
isInfraMonitoring?: boolean;
}

View File

@@ -1,4 +1,3 @@
import { getInfraAttributesValues } from 'api/infraMonitoring/getInfraAttributeValues';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import {
@@ -44,7 +43,6 @@ export const useFetchKeysAndValues = (
query: IBuilderQuery,
searchKey: string,
shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,
): IuseFetchKeysAndValues => {
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
@@ -93,10 +91,10 @@ export const useFetchKeysAndValues = (
const isQueryEnabled = useMemo(
() =>
query.dataSource === DataSource.METRICS && !isInfraMonitoring
query.dataSource === DataSource.METRICS
? !!query.dataSource && !!query.aggregateAttribute.dataType
: true,
[isInfraMonitoring, query.aggregateAttribute.dataType, query.dataSource],
[query.aggregateAttribute.dataType, query.dataSource],
);
const { data, isFetching, status } = useGetAggregateKeys(
@@ -111,7 +109,6 @@ export const useFetchKeysAndValues = (
queryKey: [searchParams],
enabled: isQueryEnabled && !shouldUseSuggestions,
},
isInfraMonitoring,
);
const {
@@ -139,7 +136,6 @@ export const useFetchKeysAndValues = (
value: string,
query: IBuilderQuery,
keys: BaseAutocompleteData[],
// eslint-disable-next-line sonarjs/cognitive-complexity
): Promise<void> => {
if (!value) {
return;
@@ -156,36 +152,17 @@ export const useFetchKeysAndValues = (
setAggregateFetching(true);
try {
let payload;
if (isInfraMonitoring) {
const response = await getInfraAttributesValues({
dataSource: query.dataSource,
attributeKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType:
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
tagType: filterAttributeKey?.type ?? '',
searchText: isInNInOperator(tagOperator)
? tagValue[tagValue.length - 1]?.toString() ?? ''
: tagValue?.toString() ?? '',
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
});
payload = response.payload;
} else {
const response = await getAttributesValues({
aggregateOperator: query.aggregateOperator,
dataSource: query.dataSource,
aggregateAttribute: query.aggregateAttribute.key,
attributeKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType:
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
tagType: filterAttributeKey?.type ?? '',
searchText: isInNInOperator(tagOperator)
? tagValue[tagValue.length - 1]?.toString() ?? ''
: tagValue?.toString() ?? '',
});
payload = response.payload;
}
const { payload } = await getAttributesValues({
aggregateOperator: query.aggregateOperator,
dataSource: query.dataSource,
aggregateAttribute: query.aggregateAttribute.key,
attributeKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType: filterAttributeKey?.dataType ?? DataTypes.EMPTY,
tagType: filterAttributeKey?.type ?? '',
searchText: isInNInOperator(tagOperator)
? tagValue[tagValue.length - 1]?.toString() ?? '' // last element of tagvalue will be always user search value
: tagValue?.toString() ?? '',
});
if (payload) {
const values = Object.values(payload).find((el) => !!el) || [];

View File

@@ -11,7 +11,6 @@ type UseGetAttributeKeys = (
options?: UseQueryOptions<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>,
isInfraMonitoring?: boolean,
) => UseQueryResult<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>;
@@ -19,22 +18,17 @@ type UseGetAttributeKeys = (
export const useGetAggregateKeys: UseGetAttributeKeys = (
requestData,
options,
isInfraMonitoring,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [
QueryBuilderKeys.GET_AGGREGATE_KEYS,
...options.queryKey,
isInfraMonitoring,
];
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, ...options.queryKey];
}
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData, isInfraMonitoring];
}, [options?.queryKey, requestData, isInfraMonitoring]);
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({
queryKey,
queryFn: () => getAggregateKeys({ ...requestData, isInfraMonitoring }),
queryFn: () => getAggregateKeys(requestData),
...options,
});
};

View File

@@ -30,12 +30,12 @@
<meta
data-react-helmet="true"
property="og:image"
content="https://signoz.io/img/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="https://signoz.io/img/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
@@ -51,26 +51,10 @@
<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="https://unpkg.com/uplot@1.6.26/dist/uPlot.min.css"
/>
<link rel="stylesheet" href="/css/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

@@ -12,7 +12,7 @@ import {
} from 'container/TopNav/DateTimeSelectionV2/config';
import { Pagination } from 'hooks/queryPagination';
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
import { isEmpty } from 'lodash-es';
import { isEmpty, cloneDeep } from 'lodash-es';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -24,7 +24,6 @@ export async function GetMetricQueryRange(
version: string,
signal?: AbortSignal,
headers?: Record<string, string>,
isInfraMonitoring?: boolean,
): Promise<SuccessResponse<MetricRangePayloadProps>> {
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
const response = await getMetricsQueryRange(

View File

@@ -1,9 +1,11 @@
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();
@@ -95,6 +97,7 @@ export const getUPlotChartData = (
const processAnomalyDetectionData = (
anomalyDetectionData: any,
isDarkMode: boolean,
): Record<string, { data: number[][]; color: string }> => {
if (!anomalyDetectionData) {
return {};
@@ -126,7 +129,8 @@ const processAnomalyDetectionData = (
legend || '',
);
const objKey = `${queryName}-${label}`;
const objKey =
anomalyDetectionData.length > 1 ? `${queryName}-${label}` : label;
processedData[objKey] = {
data: [
@@ -136,7 +140,10 @@ const processAnomalyDetectionData = (
upperBoundSeries[index].values.map((v: { value: number }) => v.value),
lowerBoundSeries[index].values.map((v: { value: number }) => v.value),
],
color: colors[index],
color: generateColor(
objKey,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
),
legendLabel: label,
};
}
@@ -146,7 +153,8 @@ const processAnomalyDetectionData = (
};
export const getUplotChartDataForAnomalyDetection = (
apiResponse?: MetricRangePayloadProps,
apiResponse: MetricRangePayloadProps,
isDarkMode: boolean,
): Record<
string,
{
@@ -156,6 +164,5 @@ export const getUplotChartDataForAnomalyDetection = (
}
> => {
const anomalyDetectionData = apiResponse?.data?.newResult?.data?.result;
return processAnomalyDetectionData(anomalyDetectionData);
return processAnomalyDetectionData(anomalyDetectionData, isDarkMode);
};

View File

@@ -233,6 +233,47 @@ 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);
@@ -244,7 +285,9 @@ function getMinMax(data: any): { minValue: number; maxValue: number } {
const minValue = flattened.length ? Math.min(...flattened) : 0;
const maxValue = Math.max(...flattened);
return { minValue, maxValue };
const { adjustedMin, adjustedMax } = adjustMinMax(minValue, maxValue);
return { minValue: adjustedMin, maxValue: adjustedMax };
}
export const getYAxisScaleForAnomalyDetection = ({

View File

@@ -11,20 +11,19 @@
.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

@@ -1,141 +0,0 @@
import { Table, TablePaginationConfig, TableProps, Typography } from 'antd';
import { SorterResult } from 'antd/es/table/interface';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import { HostMetricsLoading } from 'container/HostMetricsLoading/HostMetricsLoading';
import NoLogs from 'container/NoLogs/NoLogs';
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import HostsListControls from './HostsListControls';
import {
formatDataForTable,
getHostListsQuery,
getHostsListColumns,
HostRowData,
} from './utils';
function HostsList(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
items: [],
op: 'and',
});
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
const pageSize = 10;
const query = useMemo(() => {
const baseQuery = getHostListsQuery();
return {
...baseQuery,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
};
}, [currentPage, filters, minTime, maxTime, orderBy]);
const { data, isFetching, isLoading, isError } = useGetHostList(
query as HostListPayload,
{
queryKey: ['hostList', query],
enabled: !!query,
},
);
const hostMetricsData = useMemo(() => data?.payload?.data?.records || [], [
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const formattedHostMetricsData = useMemo(
() => formatDataForTable(hostMetricsData),
[hostMetricsData],
);
const columns = useMemo(() => getHostsListColumns(), []);
const isDataPresent =
!isLoading && !isFetching && !isError && hostMetricsData.length === 0;
const handleTableChange: TableProps<HostRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorter: SorterResult<HostRowData> | SorterResult<HostRowData>[],
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
}
if ('field' in sorter && sorter.order) {
setOrderBy({
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
});
} else {
setOrderBy(null);
}
},
[],
);
const handleFiltersChange = useCallback(
(value: IBuilderQuery['filters']): void => {
setFilters(value);
},
[],
);
return (
<div>
<HostsListControls handleFiltersChange={handleFiltersChange} />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{isLoading && <HostMetricsLoading />}
{isDataPresent && filters.items.length === 0 && (
<NoLogs dataSource={DataSource.METRICS} />
)}
{isDataPresent && filters.items.length > 0 && (
<div>No hosts match the applied filters.</div>
)}
{!isError && formattedHostMetricsData.length > 0 && (
<Table
dataSource={formattedHostMetricsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: false,
hideOnSinglePage: true,
}}
scroll={{ x: true }}
loading={isFetching}
tableLayout="fixed"
rowKey={(record): string => record.hostName}
onChange={handleTableChange}
/>
)}
</div>
);
}
export default HostsList;

View File

@@ -1,58 +0,0 @@
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useCallback, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
function HostsListControls({
handleFiltersChange,
}: {
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
}): JSX.Element {
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query,
isListViewPanel: true,
entityVersion: '',
});
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
handleChangeQueryData('filters', value);
handleFiltersChange(value);
},
[handleChangeQueryData, handleFiltersChange],
);
return (
<div className="hosts-list-controls">
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
isInfraMonitoring
/>
</div>
);
}
export default HostsListControls;

View File

@@ -1,59 +0,0 @@
.infra-monitoring-container {
display: flex;
height: 100%;
margin-top: 1rem;
.time-selector {
position: absolute;
top: 9px;
right: 0;
padding: 0 1.5rem;
}
.infra-monitoring-header {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 16px;
.tabs-wrapper {
flex: 1;
margin-right: 24px;
.infra-monitoring-tabs {
width: 100%;
:global(.ant-tabs-nav) {
margin: 0;
}
}
}
.time-selector {
flex-shrink: 0;
}
}
.hosts-list-controls {
margin: 1rem 0.5rem;
}
.progress-container {
display: flex;
align-items: center;
}
.progress-bar {
flex: 1;
margin-right: 8px;
}
.clickable-row {
cursor: pointer;
}
.infra-monitoring-tags {
border-radius: 10px;
width: fit-content;
}
}

View File

@@ -1,36 +0,0 @@
import './InfraMonitoring.styles.scss';
import * as Sentry from '@sentry/react';
import { Tabs } from 'antd';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { getTabsItems } from './utils';
function InfraMonitoringHosts(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="infra-monitoring-container">
<div className="infra-monitoring-header">
<div className="tabs-wrapper">
<Tabs
defaultActiveKey="list"
items={getTabsItems()}
className="infra-monitoring-tabs"
type="card"
/>
</div>
</div>
<div className="time-selector">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
/>
</div>
</div>
</Sentry.ErrorBoundary>
);
}
export default InfraMonitoringHosts;

View File

@@ -1,121 +0,0 @@
import './InfraMonitoring.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Progress, TabsProps, Tag } from 'antd';
import { ColumnType } from 'antd/es/table';
import { HostData, HostListPayload } from 'api/infraMonitoring/getHostLists';
import TabLabel from 'components/TabLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import HostsList from './HostsList';
export interface HostRowData {
hostName: string;
cpu: React.ReactNode;
memory: React.ReactNode;
ioWait: number;
load15: number;
active: React.ReactNode;
}
export const getHostListsQuery = (): HostListPayload => ({
filters: {
items: [],
op: 'and',
},
groupBy: [],
orderBy: { columnName: '', order: 'asc' },
});
export const getTabsItems = (): TabsProps['items'] => [
{
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
key: PANEL_TYPES.LIST,
children: <HostsList />,
},
];
export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
{
title: 'Hostname',
dataIndex: 'hostName',
key: 'hostName',
width: 150,
},
{
title: 'Status',
dataIndex: 'active',
key: 'active',
width: 100,
},
{
title: 'CPU Usage',
dataIndex: 'cpu',
key: 'cpu',
width: 100,
sorter: true,
},
{
title: 'Memory Usage',
dataIndex: 'memory',
key: 'memory',
width: 100,
sorter: true,
},
{
title: 'IOWait',
dataIndex: 'wait',
key: 'wait',
width: 100,
sorter: true,
},
{
title: 'Load Avg',
dataIndex: 'load15',
key: 'load15',
width: 100,
sorter: true,
},
];
export const formatDataForTable = (data: HostData[]): HostRowData[] =>
data.map((host, index) => ({
key: `${host.hostName}-${index}`,
hostName: host.hostName || '',
active: (
<Tag color={host.active ? 'success' : 'default'} bordered>
{host.active ? 'ACTIVE' : 'INACTIVE'}
</Tag>
),
cpu: (
<div className="progress-container">
<Progress
percent={Number((host.cpu * 100).toFixed(1))}
size="small"
strokeColor={((): string => {
const cpuPercent = Number((host.cpu * 100).toFixed(1));
if (cpuPercent >= 90) return Color.BG_SAKURA_500;
if (cpuPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
memory: (
<div className="progress-container">
<Progress
percent={Number((host.memory * 100).toFixed(1))}
size="small"
strokeColor={((): string => {
const memoryPercent = Number((host.memory * 100).toFixed(1));
if (memoryPercent >= 90) return Color.BG_CHERRY_500;
if (memoryPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
ioWait: host.wait,
load15: host.load15,
}));

View File

@@ -1,51 +0,0 @@
.infra-monitoring-module-container {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs {
flex: 1;
}
.ant-tabs-nav {
padding: 0;
margin-bottom: 0px;
&::before {
border-bottom: 1px solid var(--bg-slate-400) !important;
}
}
.ant-tabs-content-holder {
display: flex;
.ant-tabs-content {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.tab-item {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.lightMode {
.infra-monitoring-module-container {
.ant-tabs-nav {
&::before {
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -1,20 +0,0 @@
import './InfrastructureMonitoring.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import history from 'lib/history';
import { useLocation } from 'react-use';
import { Hosts } from './constants';
export default function InfrastructureMonitoringPage(): JSX.Element {
const { pathname } = useLocation();
const routes: TabRoutes[] = [Hosts];
return (
<div className="infra-monitoring-module-container">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
);
}

View File

@@ -1,15 +0,0 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import { Inbox } from 'lucide-react';
import InfraMonitoringHosts from 'pages/InfraMonitoringHosts';
export const Hosts: TabRoutes = {
Component: InfraMonitoringHosts,
name: (
<div className="tab-item">
<Inbox size={16} /> Hosts
</div>
),
route: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
};

View File

@@ -1,3 +0,0 @@
import InfrastructureMonitoringPage from './InfrastructureMonitoringPage';
export default InfrastructureMonitoringPage;

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://github.com/shivanshuraj1333/kafka-opentelemetry-instrumentation/tree/master';
'https://signoz.io/docs/messaging-queues/kafka?utm_source=product&utm_medium=kafka-get-started';
export function convertToTitleCase(text: string): string {
return text

View File

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

View File

@@ -274,3 +274,28 @@ 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

@@ -3,10 +3,9 @@ import { DataSource } from 'types/common/queryBuilder';
import { BaseAutocompleteData } from './queryAutocompleteResponse';
export interface IGetAttributeKeysPayload {
aggregateOperator?: string;
aggregateOperator: string;
dataSource: DataSource;
searchText: string;
aggregateAttribute?: string;
aggregateAttribute: string;
tagType?: BaseAutocompleteData['type'];
isInfraMonitoring?: boolean;
}

View File

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

View File

@@ -1,5 +1,6 @@
import { orange } from '@ant-design/colors';
import { Color } from '@signozhq/design-tokens';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
export const getDefaultLogBackground = (
isReadOnly?: boolean,
@@ -17,10 +18,28 @@ export const getDefaultLogBackground = (
export const getActiveLogBackground = (
isActiveLog = true,
isDarkMode = true,
logType?: string,
): string => {
if (!isActiveLog) return ``;
if (isDarkMode) return `background-color: ${Color.BG_SLATE_200};`;
return `background-color: ${Color.BG_VANILLA_300}; color: ${Color.TEXT_SLATE_400}`;
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;`;
};
export const getHightLightedLogBackground = (

View File

@@ -103,5 +103,4 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
INFRASTRUCTURE_MONITORING_HOSTS: ['ADMIN', 'EDITOR', 'VIEWER'],
};

View File

@@ -486,6 +486,7 @@ 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)
@@ -1976,6 +1977,32 @@ 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.StatusInternalServerError)
} 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) {
@@ -2489,22 +2516,35 @@ func (aH *APIHandler) RegisterMessagingQueuesRoutes(router *mux.Router, am *Auth
// SubRouter for kafka
kafkaRouter := router.PathPrefix("/api/v1/messaging-queues/kafka").Subrouter()
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)
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)
// for other messaging queues, add SubRouters here
}
// not using md5 hashing as the plain string would work
func uniqueIdentifier(clientID, serviceInstanceID, serviceName, separator string) string {
return clientID + separator + serviceInstanceID + separator + serviceName
func uniqueIdentifier(params []string, separator string) string {
return strings.Join(params, separator)
}
func (aH *APIHandler) onboardProducers(
@@ -2847,7 +2887,7 @@ func (aH *APIHandler) getNetworkData(
return
}
queryRangeParams, err := mq.BuildQRParamsNetwork(messagingQueue, "throughput", attributeCache)
queryRangeParams, err := mq.BuildQRParamsWithCache(messagingQueue, "throughput", attributeCache)
if err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
@@ -2874,7 +2914,8 @@ func (aH *APIHandler) getNetworkData(
clientID, clientIDOk := series.Labels["client_id"]
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
serviceName, serviceNameOk := series.Labels["service_name"]
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
params := []string{clientID, serviceInstanceID, serviceName}
hashKey := uniqueIdentifier(params, "#")
_, ok := attributeCache.Hash[hashKey]
if clientIDOk && serviceInstanceIDOk && serviceNameOk && !ok {
attributeCache.Hash[hashKey] = struct{}{}
@@ -2885,7 +2926,7 @@ func (aH *APIHandler) getNetworkData(
}
}
queryRangeParams, err = mq.BuildQRParamsNetwork(messagingQueue, "fetch-latency", attributeCache)
queryRangeParams, err = mq.BuildQRParamsWithCache(messagingQueue, "fetch-latency", attributeCache)
if err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
@@ -2911,7 +2952,8 @@ func (aH *APIHandler) getNetworkData(
clientID, clientIDOk := series.Labels["client_id"]
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
serviceName, serviceNameOk := series.Labels["service_name"]
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
params := []string{clientID, serviceInstanceID, serviceName}
hashKey := uniqueIdentifier(params, "#")
_, ok := attributeCache.Hash[hashKey]
if clientIDOk && serviceInstanceIDOk && serviceNameOk && ok {
latencySeries = append(latencySeries, series)
@@ -3013,6 +3055,362 @@ 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, "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)
}
// 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

@@ -28,6 +28,7 @@ Response in query range `table` format
"resultType": "",
"result": [
{
"table": {
"columns": [
{
@@ -519,3 +520,461 @@ Response in query range `table` format
]
}
```
### Partition Latency
```json
/api/v1/messaging-queues/kafka/partition-latency/overview
```
```json
{
"start": 1728287046000000000,
"end": 1728587046000000000
}
```
```json
{
"status": "success",
"data": {
"resultType": "",
"result": [
{
"table": {
"columns": [
{
"name": "topic",
"queryName": "",
"isValueColumn": false
},
{
"name": "p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "partition_latency",
"queryName": "partition_latency",
"isValueColumn": true
}
],
"rows": [
{
"data": {
"p99": "2",
"partition_latency": 1.18,
"topic": "topic1"
}
},
{
"data": {
"p99": "2",
"partition_latency": 0.15,
"topic": "topic2"
}
},
{
"data": {
"p99": "2",
"partition_latency": 0.26,
"topic": "topic3"
}
}
]
}
}
]
}
}
```
---------
```json
/api/v1/messaging-queues/kafka/partition-latency/consumer
```
```json
{
"start": 1728287046000000000,
"end": 1728587046000000000,
"variables": {
"partition": "2",
"topic": "topic1"
}
}
```
```json
{
"status": "success",
"data": {
"resultType": "",
"result": [
{
"table": {
"columns": [
{
"name": "consumer_group",
"queryName": "",
"isValueColumn": false
},
{
"name": "service_name",
"queryName": "",
"isValueColumn": false
},
{
"name": "p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "error_rate",
"queryName": "",
"isValueColumn": false
},
{
"name": "throughput",
"queryName": "",
"isValueColumn": false
}
],
"rows": [
{
"data": {
"consumer_group": "cg1",
"error_rate": "0",
"p99": "0.11994228000000004",
"service_name": "consumer-svc",
"throughput": "1.18116"
}
}
]
}
}
]
}
}
```
---------
### Topic throughput
```json
/api/v1/messaging-queues/kafka/topic-throughput/producer
```
```json
{
"start": 1728287046000000000,
"end": 1728587046000000000
}
```
```json
{
"status": "success",
"data": {
"resultType": "",
"result": [
{
"table": {
"columns": [
{
"name": "topic",
"queryName": "",
"isValueColumn": false
},
{
"name": "serviceName",
"queryName": "",
"isValueColumn": false
},
{
"name": "p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "error_rate",
"queryName": "",
"isValueColumn": false
},
{
"name": "throughput",
"queryName": "",
"isValueColumn": false
}
],
"rows": [
{
"data": {
"error_rate": "0",
"p99": "8.662880220000002",
"serviceName": "producer-svc1",
"throughput": "0.41642666666666667",
"topic": "topic1"
}
},
{
"data": {
"error_rate": "0",
"p99": "9.786847500000016",
"serviceName": "producer-svc2",
"throughput": "0.76473",
"topic": "topic1"
}
},
{
"data": {
"error_rate": "0",
"p99": "14.432925500000021",
"serviceName": "producer-svc3",
"throughput": "0.08976",
"topic": "topic2"
}
},
{
"data": {
"error_rate": "0",
"p99": "14.32833297000002",
"serviceName": "producer-svc2",
"throughput": "0.06449333333333333",
"topic": "topic2"
}
},
{
"data": {
"error_rate": "0",
"p99": "13.416533810000036",
"serviceName": "producer-svc4",
"throughput": "0.14766",
"topic": "topic3"
}
},
{
"data": {
"error_rate": "0",
"p99": "13.366232000000034",
"serviceName": "producer-svc3",
"throughput": "0.11166666666666666",
"topic": "topic3"
}
}
]
}
}
]
}
}
```
---------
### Topic throughput
```json
/api/v1/messaging-queues/kafka/topic-throughput/producer-details
```
```json
{
"start": 1728287046000000000,
"end": 1728587046000000000,
"variables": {
"partition": "2",
"topic": "topic1",
"service_name": "producer-svc2"
}
}
```
```json
{
"status": "success",
"data": {
"resultType": "",
"result": [
{
"table": {
"columns": [
{
"name": "partition",
"queryName": "",
"isValueColumn": false
},
{
"name": "p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "error_rate",
"queryName": "",
"isValueColumn": false
},
{
"name": "throughput",
"queryName": "",
"isValueColumn": false
}
],
"rows": [
{
"data": {
"error_rate": "0",
"p99": "9.165558780000026",
"partition": "2",
"throughput": "0.76473"
}
}
]
}
}
]
}
}
```
---------
### Topic throughput
```json
/api/v1/messaging-queues/kafka/topic-throughput/consumer
```
```json
{
"start": 1728287046000000000,
"end": 1728587046000000000
}
```
```json
{
"status": "success",
"data": {
"resultType": "",
"result": [
{
"table": {
"columns": [
{
"name": "topic",
"queryName": "",
"isValueColumn": false
},
{
"name": "service_name",
"queryName": "",
"isValueColumn": false
},
{
"name": "p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "error_rate",
"queryName": "",
"isValueColumn": false
},
{
"name": "ingestion_rate",
"queryName": "",
"isValueColumn": false
},
{
"name": "byte_rate",
"queryName": "",
"isValueColumn": false
}
],
"rows": [
{
"data": {
"byte_rate": "17.7174",
"error_rate": "0",
"ingestion_rate": "1.18116",
"p99": "0.12260112000000009",
"service_name": "consumer-svc",
"topic": "topic1"
}
},
{
"data": {
"byte_rate": "2.1594533333333334",
"error_rate": "0",
"ingestion_rate": "0.15424666666666667",
"p99": "7.4079657800000005",
"service_name": "consumer-svc2",
"topic": "topic2"
}
},
{
"data": {
"byte_rate": "3.66446",
"error_rate": "0",
"ingestion_rate": "0.25933",
"p99": "6.135769970000011",
"service_name": "consumer-svc3",
"topic": "topic3"
}
}
]
}
}
]
}
}
```
---------
### Topic throughput
```json
/api/v1/messaging-queues/kafka/topic-throughput/consumer-details
```
```json
{
"start": 1728287046000000000,
"end": 1728587046000000000,
"variables": {
"topic": "topic1",
"service_name": "consumer-svc"
}
}
```
```json
{
"status": "success",
"data": {
"resultType": "",
"result": [
{
"table": {
"columns": [
{
"name": "partition",
"queryName": "",
"isValueColumn": false
},
{
"name": "p99",
"queryName": "",
"isValueColumn": false
},
{
"name": "error_rate",
"queryName": "",
"isValueColumn": false
},
{
"name": "throughput",
"queryName": "",
"isValueColumn": false
}
],
"rows": [
{
"data": {
"error_rate": "0",
"p99": "0.11789381000000003",
"partition": "2",
"throughput": "1.18116"
}
}
]
}
}
]
}
}
```

View File

@@ -5,6 +5,7 @@ const KafkaQueue = "kafka"
type MessagingQueue struct {
Start int64 `json:"start"`
End int64 `json:"end"`
EvalTime int64 `json:"eval_time"`
Variables map[string]string `json:"variables,omitempty"`
}
@@ -13,6 +14,7 @@ type Clients struct {
ClientID []string
ServiceInstanceID []string
ServiceName []string
TopicName []string
}
type OnboardingResponse struct {

View File

@@ -12,7 +12,7 @@ WITH consumer_query AS (
serviceName,
quantile(0.99)(durationNano) / 1000000 AS p99,
COUNT(*) AS total_requests,
SUM(CASE WHEN statusCode = 2 THEN 1 ELSE 0 END) AS error_count,
sumIf(1, statusCode = 2) AS error_count,
avg(CASE WHEN has(numberTagMap, 'messaging.message.body.size') THEN numberTagMap['messaging.message.body.size'] ELSE NULL END) AS avg_msg_size
FROM signoz_traces.distributed_signoz_index_v2
WHERE
@@ -30,7 +30,7 @@ SELECT
serviceName AS service_name,
p99,
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
COALESCE(total_requests / %d, 0) AS throughput, -- Convert nanoseconds to seconds
COALESCE(total_requests / %d, 0) AS throughput,
COALESCE(avg_msg_size, 0) AS avg_msg_size
FROM
consumer_query
@@ -40,6 +40,257 @@ ORDER BY
return query
}
// S1 landing
func generatePartitionLatencySQL(start, end int64, queueType string) string {
timeRange := (end - start) / 1000000000
query := fmt.Sprintf(`
WITH partition_query AS (
SELECT
quantile(0.99)(durationNano) / 1000000 AS p99,
count(*) AS total_requests,
stringTagMap['messaging.destination.name'] AS topic,
stringTagMap['messaging.destination.partition.id'] AS partition
FROM signoz_traces.distributed_signoz_index_v2
WHERE
timestamp >= '%d'
AND timestamp <= '%d'
AND kind = 4
AND msgSystem = '%s'
GROUP BY topic, partition
)
SELECT
topic,
partition,
p99,
COALESCE(total_requests / %d, 0) AS throughput
FROM
partition_query
ORDER BY
topic;
`, start, end, queueType, timeRange)
return query
}
// S1 consumer
func generateConsumerPartitionLatencySQL(start, end int64, topic, partition, queueType string) string {
timeRange := (end - start) / 1000000000
query := fmt.Sprintf(`
WITH consumer_pl AS (
SELECT
stringTagMap['messaging.kafka.consumer.group'] AS consumer_group,
serviceName,
quantile(0.99)(durationNano) / 1000000 AS p99,
COUNT(*) AS total_requests,
sumIf(1, statusCode = 2) AS error_count
FROM signoz_traces.distributed_signoz_index_v2
WHERE
timestamp >= '%d'
AND timestamp <= '%d'
AND kind = 5
AND msgSystem = '%s'
AND stringTagMap['messaging.destination.name'] = '%s'
AND stringTagMap['messaging.destination.partition.id'] = '%s'
GROUP BY consumer_group, serviceName
)
SELECT
consumer_group,
serviceName AS service_name,
p99,
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
COALESCE(total_requests / %d, 0) AS throughput
FROM
consumer_pl
ORDER BY
consumer_group;
`, start, end, queueType, topic, partition, timeRange)
return query
}
// S3, producer overview
func generateProducerPartitionThroughputSQL(start, end int64, queueType string) string {
timeRange := (end - start) / 1000000000
// t, svc, rps, byte*, p99, err
query := fmt.Sprintf(`
WITH producer_latency AS (
SELECT
serviceName,
quantile(0.99)(durationNano) / 1000000 AS p99,
stringTagMap['messaging.destination.name'] AS topic,
COUNT(*) AS total_requests,
sumIf(1, statusCode = 2) AS error_count
FROM signoz_traces.distributed_signoz_index_v2
WHERE
timestamp >= '%d'
AND timestamp <= '%d'
AND kind = 4
AND msgSystem = '%s'
GROUP BY topic, serviceName
)
SELECT
topic,
serviceName AS service_name,
p99,
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
COALESCE(total_requests / %d, 0) AS throughput
FROM
producer_latency
`, start, end, queueType, timeRange)
return query
}
// S3, producer topic/service overview
func generateProducerTopicLatencySQL(start, end int64, topic, service, queueType string) string {
timeRange := (end - start) / 1000000000
query := fmt.Sprintf(`
WITH consumer_latency AS (
SELECT
quantile(0.99)(durationNano) / 1000000 AS p99,
stringTagMap['messaging.destination.partition.id'] AS partition,
COUNT(*) AS total_requests,
sumIf(1, statusCode = 2) AS error_count
FROM signoz_traces.distributed_signoz_index_v2
WHERE
timestamp >= '%d'
AND timestamp <= '%d'
AND kind = 4
AND serviceName = '%s'
AND msgSystem = '%s'
AND stringTagMap['messaging.destination.name'] = '%s'
GROUP BY partition
)
SELECT
partition,
p99,
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
COALESCE(total_requests / %d, 0) AS throughput
FROM
consumer_latency
`, start, end, service, queueType, topic, timeRange)
return query
}
// S3 consumer overview
func generateConsumerLatencySQL(start, end int64, queueType string) string {
timeRange := (end - start) / 1000000000
query := fmt.Sprintf(`
WITH consumer_latency AS (
SELECT
serviceName,
stringTagMap['messaging.destination.name'] AS topic,
quantile(0.99)(durationNano) / 1000000 AS p99,
COUNT(*) AS total_requests,
sumIf(1, statusCode = 2) AS error_count,
SUM(numberTagMap['messaging.message.body.size']) AS total_bytes
FROM signoz_traces.distributed_signoz_index_v2
WHERE
timestamp >= '%d'
AND timestamp <= '%d'
AND kind = 5
AND msgSystem = '%s'
GROUP BY topic, serviceName
)
SELECT
topic,
serviceName AS service_name,
p99,
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
COALESCE(total_requests / %d, 0) AS ingestion_rate,
COALESCE(total_bytes / %d, 0) AS byte_rate
FROM
consumer_latency
ORDER BY
topic;
`, start, end, queueType, timeRange, timeRange)
return query
}
// S3 consumer topic/service
func generateConsumerServiceLatencySQL(start, end int64, topic, service, queueType string) string {
timeRange := (end - start) / 1000000000
query := fmt.Sprintf(`
WITH consumer_latency AS (
SELECT
quantile(0.99)(durationNano) / 1000000 AS p99,
stringTagMap['messaging.destination.partition.id'] AS partition,
COUNT(*) AS total_requests,
sumIf(1, statusCode = 2) AS error_count
FROM signoz_traces.distributed_signoz_index_v2
WHERE
timestamp >= '%d'
AND timestamp <= '%d'
AND kind = 5
AND serviceName = '%s'
AND msgSystem = '%s'
AND stringTagMap['messaging.destination.name'] = '%s'
GROUP BY partition
)
SELECT
partition,
p99,
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
COALESCE(total_requests / %d, 0) AS throughput
FROM
consumer_latency
`, start, end, service, queueType, topic, timeRange)
return query
}
// s4
func generateProducerConsumerEvalSQL(start, end int64, queueType string, evalTime int64) string {
query := fmt.Sprintf(`
WITH trace_data AS (
SELECT
p.serviceName AS producer_service,
c.serviceName AS consumer_service,
p.traceID,
p.timestamp AS producer_timestamp,
c.timestamp AS consumer_timestamp,
p.durationNano AS durationNano,
(toUnixTimestamp64Nano(c.timestamp) - toUnixTimestamp64Nano(p.timestamp)) + p.durationNano AS time_difference
FROM
signoz_traces.distributed_signoz_index_v2 p
INNER JOIN
signoz_traces.distributed_signoz_index_v2 c
ON p.traceID = c.traceID
AND c.parentSpanID = p.spanID
WHERE
p.kind = 4
AND c.kind = 5
AND toUnixTimestamp64Nano(p.timestamp) BETWEEN '%d' AND '%d'
AND toUnixTimestamp64Nano(c.timestamp) BETWEEN '%d' AND '%d'
AND c.msgSystem = '%s'
AND p.msgSystem = '%s'
)
SELECT
producer_service,
consumer_service,
COUNT(*) AS total_spans,
SUM(time_difference > '%d') AS breached_spans,
((breached_spans) * 100.0) / total_spans AS breach_percentage,
arraySlice(
arrayMap(x -> x.1,
arraySort(
x -> -x.2,
groupArrayIf((traceID, time_difference), time_difference > '%d')
)
),
1, 10
) AS top_traceIDs
FROM trace_data
GROUP BY
producer_service,
consumer_service
`, start, end, start, end, queueType, queueType, evalTime, evalTime)
return query
}
func generateProducerSQL(start, end int64, topic, partition, queueType string) string {
timeRange := (end - start) / 1000000000
query := fmt.Sprintf(`
@@ -48,7 +299,7 @@ WITH producer_query AS (
serviceName,
quantile(0.99)(durationNano) / 1000000 AS p99,
count(*) AS total_count,
SUM(CASE WHEN statusCode = 2 THEN 1 ELSE 0 END) AS error_count
sumIf(1, statusCode = 2) AS error_count
FROM signoz_traces.distributed_signoz_index_v2
WHERE
timestamp >= '%d'
@@ -64,12 +315,11 @@ SELECT
serviceName AS service_name,
p99,
COALESCE((error_count * 100.0) / total_count, 0) AS error_percentage,
COALESCE(total_count / %d, 0) AS throughput -- Convert nanoseconds to seconds
COALESCE(total_count / %d, 0) AS throughput
FROM
producer_query
ORDER BY
serviceName;
`, start, end, queueType, topic, partition, timeRange)
return query
}

View File

@@ -2,9 +2,8 @@ package kafka
import (
"fmt"
"strings"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/constants"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
)
@@ -12,6 +11,9 @@ var defaultStepInterval int64 = 60
func BuildQueryRangeParams(messagingQueue *MessagingQueue, queryContext string) (*v3.QueryRangeParamsV3, error) {
if constants.KafkaSpanEval == "false" && queryContext == "producer-consumer-eval" {
return nil, fmt.Errorf("span evaluation feature is disabled and is experimental")
}
// ToDo: propagate this through APIs when there are different handlers
queueType := KafkaQueue
@@ -37,14 +39,6 @@ func BuildQueryRangeParams(messagingQueue *MessagingQueue, queryContext string)
return queryRangeParams, nil
}
func PrepareClikhouseQueries(messagingQueue *MessagingQueue, queryContext string) (*v3.ClickHouseQuery, error) {
queueType := KafkaQueue
chq, err := BuildClickHouseQuery(messagingQueue, queueType, queryContext)
return chq, err
}
func buildClickHouseQueryNetwork(messagingQueue *MessagingQueue, queueType string) (*v3.ClickHouseQuery, error) {
start := messagingQueue.Start
end := messagingQueue.End
@@ -65,12 +59,60 @@ func buildClickHouseQueryNetwork(messagingQueue *MessagingQueue, queueType strin
}, nil
}
func formatstring(str []string) string {
joined := strings.Join(str, ", ")
if len(joined) <= 2 {
return ""
func buildBuilderQueriesProducerBytes(unixMilliStart, unixMilliEnd int64, attributeCache *Clients) (map[string]*v3.BuilderQuery, error) {
bq := make(map[string]*v3.BuilderQuery)
queryName := fmt.Sprintf("latency")
chq := &v3.BuilderQuery{
QueryName: queryName,
StepInterval: common.MinAllowedStepInterval(unixMilliStart, unixMilliEnd),
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: "kafka_producer_byte_rate",
},
AggregateOperator: v3.AggregateOperatorAvg,
Temporality: v3.Unspecified,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationAvg,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "service_name",
Type: v3.AttributeKeyTypeTag,
DataType: v3.AttributeKeyDataTypeString,
},
Operator: v3.FilterOperatorIn,
Value: attributeCache.ServiceName,
},
{
Key: v3.AttributeKey{
Key: "topic",
Type: v3.AttributeKeyTypeTag,
DataType: v3.AttributeKeyDataTypeString,
},
Operator: v3.FilterOperatorIn,
Value: attributeCache.TopicName,
},
},
},
Expression: queryName,
ReduceTo: v3.ReduceToOperatorAvg,
GroupBy: []v3.AttributeKey{{
Key: "service_name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
{
Key: "topic",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
},
}
return joined[1 : len(joined)-1]
bq[queryName] = chq
return bq, nil
}
func buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd int64, attributeCache *Clients) (map[string]*v3.BuilderQuery, error) {
@@ -143,7 +185,7 @@ func buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd int64, attributeCac
return bq, nil
}
func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, attributeCache *Clients) (*v3.QueryRangeParamsV3, error) {
func BuildQRParamsWithCache(messagingQueue *MessagingQueue, queryContext string, attributeCache *Clients) (*v3.QueryRangeParamsV3, error) {
queueType := KafkaQueue
@@ -151,6 +193,7 @@ func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, a
unixMilliEnd := messagingQueue.End / 1000000
var cq *v3.CompositeQuery
var err error
if queryContext == "throughput" {
chq, err := buildClickHouseQueryNetwork(messagingQueue, queueType)
@@ -171,6 +214,24 @@ func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, a
BuilderQueries: bhq,
PanelType: v3.PanelTypeTable,
}
} else if queryContext == "producer-throughput-overview" {
start := messagingQueue.Start
end := messagingQueue.End
query := generateProducerPartitionThroughputSQL(start, end, queueType)
cq, err = buildCompositeQuery(&v3.ClickHouseQuery{
Query: query,
}, queryContext)
} else if queryContext == "producer-throughput-overview-latency" {
bhq, err := buildBuilderQueriesProducerBytes(unixMilliStart, unixMilliEnd, attributeCache)
if err != nil {
return nil, err
}
cq = &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
BuilderQueries: bhq,
PanelType: v3.PanelTypeTable,
}
}
queryRangeParams := &v3.QueryRangeParamsV3{
@@ -182,7 +243,7 @@ func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, a
FormatForWeb: true,
}
return queryRangeParams, nil
return queryRangeParams, err
}
func BuildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, queryContext string) (*v3.ClickHouseQuery, error) {
@@ -190,16 +251,22 @@ func BuildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer
end := messagingQueue.End
var topic, partition string
if queryContext == "producer" || queryContext == "consumer" {
if queryContext == "producer" ||
queryContext == "consumer" ||
queryContext == "consumer_partition_latency" ||
queryContext == "producer-topic-throughput" ||
queryContext == "producer-throughput-details" ||
queryContext == "consumer-throughput-details" {
var ok bool
topic, ok = messagingQueue.Variables["topic"]
if !ok {
return nil, fmt.Errorf("invalid type for Topic")
}
partition, ok = messagingQueue.Variables["partition"]
if !ok {
return nil, fmt.Errorf("invalid type for Partition")
if queryContext != "consumer-throughput-details" {
partition, ok = messagingQueue.Variables["partition"]
if !ok {
return nil, fmt.Errorf("invalid type for Partition")
}
}
}
@@ -212,6 +279,26 @@ func BuildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer
return nil, fmt.Errorf("invalid type for consumer group")
}
query = generateConsumerSQL(start, end, topic, partition, consumerGroup, queueType)
} else if queryContext == "producer-topic-throughput" {
query = generatePartitionLatencySQL(start, end, queueType)
} else if queryContext == "consumer_partition_latency" {
query = generateConsumerPartitionLatencySQL(start, end, topic, partition, queueType)
} else if queryContext == "producer-throughput-details" {
svcName, ok := messagingQueue.Variables["service_name"]
if !ok {
return nil, fmt.Errorf("invalid type for service")
}
query = generateProducerTopicLatencySQL(start, end, topic, svcName, queueType)
} else if queryContext == "consumer-throughput-overview" {
query = generateConsumerLatencySQL(start, end, queueType)
} else if queryContext == "consumer-throughput-details" {
svcName, ok := messagingQueue.Variables["service_name"]
if !ok {
return nil, fmt.Errorf("invalid type for service")
}
query = generateConsumerServiceLatencySQL(start, end, topic, svcName, queueType)
} else if queryContext == "producer-consumer-eval" {
query = generateProducerConsumerEvalSQL(start, end, queueType, messagingQueue.EvalTime)
} else if queryContext == "onboard_producers" {
query = onboardProducersSQL(start, end, queueType)
} else if queryContext == "onboard_consumers" {
@@ -219,13 +306,21 @@ func BuildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer
} else if queryContext == "onboard_kafka" {
query = onboardKafkaSQL(start, end)
}
return &v3.ClickHouseQuery{
Query: query,
}, nil
}
func buildCompositeQuery(chq *v3.ClickHouseQuery, queryContext string) (*v3.CompositeQuery, error) {
if queryContext == "producer-consumer-eva" {
return &v3.CompositeQuery{
QueryType: v3.QueryTypeClickHouseSQL,
ClickHouseQueries: map[string]*v3.ClickHouseQuery{queryContext: chq},
PanelType: v3.PanelTypeList,
}, nil
}
return &v3.CompositeQuery{
QueryType: v3.QueryTypeClickHouseSQL,
ClickHouseQueries: map[string]*v3.ClickHouseQuery{queryContext: chq},

View File

@@ -496,7 +496,7 @@ func IsOrderByTs(orderBy []v3.OrderBy) bool {
// PrepareLogsQuery prepares the query for logs
// start and end are in epoch millisecond
// step is in seconds
func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options v3.LogQBOptions) (string, error) {
func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options v3.QBOptions) (string, error) {
// adjust the start and end time to the step interval
// NOTE: Disabling this as it's creating confusion between charts and actual data

View File

@@ -1201,7 +1201,7 @@ var testPrepLogsQueryData = []struct {
TableName string
AggregateOperator v3.AggregateOperator
ExpectedQuery string
Options v3.LogQBOptions
Options v3.QBOptions
}{
{
Name: "Test TS with limit- first",
@@ -1223,7 +1223,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT `method` from (SELECT attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') group by `method` order by value DESC) LIMIT 10",
Options: v3.LogQBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
Options: v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
},
{
Name: "Test TS with limit- first - with order by value",
@@ -1246,7 +1246,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT `method` from (SELECT attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') group by `method` order by value ASC) LIMIT 10",
Options: v3.LogQBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
Options: v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
},
{
Name: "Test TS with limit- first - with order by attribute",
@@ -1269,7 +1269,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT `method` from (SELECT attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') group by `method` order by `method` ASC) LIMIT 10",
Options: v3.LogQBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
Options: v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
},
{
Name: "Test TS with limit- second",
@@ -1291,7 +1291,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') AND (`method`) GLOBAL IN (#LIMIT_PLACEHOLDER) group by `method`,ts order by value DESC",
Options: v3.LogQBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
Options: v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
},
{
Name: "Test TS with limit- second - with order by",
@@ -1314,7 +1314,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') AND (`method`) GLOBAL IN (#LIMIT_PLACEHOLDER) group by `method`,ts order by `method` ASC",
Options: v3.LogQBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
Options: v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
},
// Live tail
{
@@ -1334,7 +1334,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND ",
Options: v3.LogQBOptions{IsLivetailQuery: true},
Options: v3.QBOptions{IsLivetailQuery: true},
},
{
Name: "Live Tail Query with contains",
@@ -1353,7 +1353,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where attributes_string_value[indexOf(attributes_string_key, 'method')] ILIKE '%GET%' AND ",
Options: v3.LogQBOptions{IsLivetailQuery: true},
Options: v3.QBOptions{IsLivetailQuery: true},
},
{
Name: "Live Tail Query W/O filter",
@@ -1369,7 +1369,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where ",
Options: v3.LogQBOptions{IsLivetailQuery: true},
Options: v3.QBOptions{IsLivetailQuery: true},
},
{
Name: "Table query w/o limit",
@@ -1385,7 +1385,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by value DESC",
Options: v3.LogQBOptions{},
Options: v3.QBOptions{},
},
{
Name: "Table query with limit",
@@ -1402,7 +1402,7 @@ var testPrepLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by value DESC LIMIT 10",
Options: v3.LogQBOptions{},
Options: v3.QBOptions{},
},
{
Name: "Ignore offset if order by is timestamp in list queries",
@@ -1488,7 +1488,7 @@ var testPrepLogsQueryLimitOffsetData = []struct {
TableName string
AggregateOperator v3.AggregateOperator
ExpectedQuery string
Options v3.LogQBOptions
Options v3.QBOptions
}{
{
Name: "Test limit less than pageSize - order by ts",

View File

@@ -5,6 +5,7 @@ import (
"strings"
logsV3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3"
"go.signoz.io/signoz/pkg/query-service/app/resource"
"go.signoz.io/signoz/pkg/query-service/constants"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/utils"
@@ -33,6 +34,7 @@ const (
BODY = "body"
DISTRIBUTED_LOGS_V2 = "distributed_logs_v2"
DISTRIBUTED_LOGS_V2_RESOURCE = "distributed_logs_v2_resource"
DB_NAME = "signoz_logs"
NANOSECOND = 1000000000
)
@@ -372,7 +374,7 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build
}
// build the where clause for resource table
resourceSubQuery, err := buildResourceSubQuery(bucketStart, bucketEnd, mq.Filters, mq.GroupBy, mq.AggregateAttribute, false)
resourceSubQuery, err := resource.BuildResourceSubQuery(DB_NAME, DISTRIBUTED_LOGS_V2_RESOURCE, bucketStart, bucketEnd, mq.Filters, mq.GroupBy, mq.AggregateAttribute, false)
if err != nil {
return "", err
}
@@ -463,7 +465,7 @@ func buildLogsLiveTailQuery(mq *v3.BuilderQuery) (string, error) {
}
// no values for bucket start and end
resourceSubQuery, err := buildResourceSubQuery(0, 0, mq.Filters, mq.GroupBy, mq.AggregateAttribute, true)
resourceSubQuery, err := resource.BuildResourceSubQuery(DB_NAME, DISTRIBUTED_LOGS_V2_RESOURCE, 0, 0, mq.Filters, mq.GroupBy, mq.AggregateAttribute, true)
if err != nil {
return "", err
}
@@ -491,7 +493,7 @@ func buildLogsLiveTailQuery(mq *v3.BuilderQuery) (string, error) {
}
// PrepareLogsQuery prepares the query for logs
func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options v3.LogQBOptions) (string, error) {
func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options v3.QBOptions) (string, error) {
// adjust the start and end time to the step interval
// NOTE: Disabling this as it's creating confusion between charts and actual data

View File

@@ -806,7 +806,7 @@ func TestPrepareLogsQuery(t *testing.T) {
queryType v3.QueryType
panelType v3.PanelType
mq *v3.BuilderQuery
options v3.LogQBOptions
options v3.QBOptions
}
tests := []struct {
name string
@@ -875,7 +875,7 @@ func TestPrepareLogsQuery(t *testing.T) {
Limit: 10,
GroupBy: []v3.AttributeKey{{Key: "user", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
},
options: v3.LogQBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
options: v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
},
want: "SELECT `user` from (SELECT attributes_string['user'] as `user`, toFloat64(count(distinct(attributes_string['name']))) as value from signoz_logs.distributed_logs_v2 " +
"where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND (ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458) AND attributes_string['method'] = 'GET' " +
@@ -904,7 +904,7 @@ func TestPrepareLogsQuery(t *testing.T) {
GroupBy: []v3.AttributeKey{{Key: "user", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
Limit: 2,
},
options: v3.LogQBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
options: v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
},
want: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string['user'] as `user`, toFloat64(count(distinct(attributes_string['name']))) as value " +
"from signoz_logs.distributed_logs_v2 where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND (ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458) AND " +
@@ -929,7 +929,7 @@ func TestPrepareLogsQuery(t *testing.T) {
},
},
},
options: v3.LogQBOptions{IsLivetailQuery: true},
options: v3.QBOptions{IsLivetailQuery: true},
},
want: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body, attributes_string, attributes_number, attributes_bool, resources_string " +
"from signoz_logs.distributed_logs_v2 where attributes_string['method'] = 'GET' AND mapContains(attributes_string, 'method') AND ",
@@ -952,7 +952,7 @@ func TestPrepareLogsQuery(t *testing.T) {
},
},
},
options: v3.LogQBOptions{IsLivetailQuery: true},
options: v3.QBOptions{IsLivetailQuery: true},
},
want: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body, attributes_string, attributes_number, attributes_bool, resources_string from " +
"signoz_logs.distributed_logs_v2 where attributes_string['method'] = 'GET' AND mapContains(attributes_string, 'method') AND " +
@@ -972,7 +972,7 @@ func TestPrepareLogsQuery(t *testing.T) {
Expression: "A",
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
},
options: v3.LogQBOptions{IsLivetailQuery: true},
options: v3.QBOptions{IsLivetailQuery: true},
},
want: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body, attributes_string, attributes_number, attributes_bool, resources_string " +
"from signoz_logs.distributed_logs_v2 where ",

View File

@@ -114,12 +114,14 @@ func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery)
samplesTableFilter := fmt.Sprintf("metric_name = %s AND unix_milli >= %d AND unix_milli < %d", utils.ClickHouseFormattedValue(mq.AggregateAttribute.Key), start, end)
tableName := helpers.WhichSamplesTableToUse(start, end, mq)
// Select the aggregate value for interval
queryTmpl :=
"SELECT fingerprint, %s" +
" toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL %d SECOND) as ts," +
" %s as per_series_value" +
" FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + constants.SIGNOZ_SAMPLES_V4_TABLENAME +
" FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + tableName +
" INNER JOIN" +
" (%s) as filtered_time_series" +
" USING fingerprint" +
@@ -130,37 +132,30 @@ func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery)
selectLabelsAny := helpers.SelectLabelsAny(mq.GroupBy)
selectLabels := helpers.SelectLabels(mq.GroupBy)
op := helpers.AggregationColumnForSamplesTable(start, end, mq)
switch mq.TimeAggregation {
case v3.TimeAggregationAvg:
op := "avg(value)"
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
case v3.TimeAggregationSum:
op := "sum(value)"
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
case v3.TimeAggregationMin:
op := "min(value)"
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
case v3.TimeAggregationMax:
op := "max(value)"
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
case v3.TimeAggregationCount:
op := "count(value)"
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
case v3.TimeAggregationCountDistinct:
op := "count(distinct(value))"
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
case v3.TimeAggregationAnyLast:
op := "anyLast(value)"
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
case v3.TimeAggregationRate:
op := "max(value)"
innerSubQuery := fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
rateQueryTmpl :=
"SELECT %s ts, " + rateWithoutNegative +
" as per_series_value FROM (%s) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)"
subQuery = fmt.Sprintf(rateQueryTmpl, selectLabels, innerSubQuery)
case v3.TimeAggregationIncrease:
op := "max(value)"
innerSubQuery := fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
rateQueryTmpl :=
"SELECT %s ts, " + increaseWithoutNegative +

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