mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend
This commit is contained in:
commit
a2ab97a347
@ -257,6 +257,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
r.Use(middleware.NewComment().Wrap)
|
||||
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
|
||||
@ -5,7 +5,10 @@ import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
BaseBuilderQuery,
|
||||
FieldContext,
|
||||
@ -276,6 +279,103 @@ export function convertBuilderQueriesToV5(
|
||||
);
|
||||
}
|
||||
|
||||
function createTraceOperatorBaseSpec(
|
||||
queryData: IBuilderTraceOperator,
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): BaseBuilderQuery {
|
||||
const nonEmptySelectColumns = (queryData.selectColumns as (
|
||||
| BaseAutocompleteData
|
||||
| TelemetryFieldKey
|
||||
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
|
||||
|
||||
return {
|
||||
stepInterval: queryData?.stepInterval || undefined,
|
||||
groupBy:
|
||||
queryData.groupBy?.length > 0
|
||||
? queryData.groupBy.map(
|
||||
(item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType,
|
||||
fieldContext: item?.type,
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
limit:
|
||||
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||
? queryData.limit || queryData.pageSize || undefined
|
||||
: queryData.limit || undefined,
|
||||
offset:
|
||||
requestType === 'raw' || requestType === 'trace'
|
||||
? queryData.offset
|
||||
: undefined,
|
||||
order:
|
||||
queryData.orderBy?.length > 0
|
||||
? queryData.orderBy.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||
having: isEmpty(queryData.having) ? undefined : (queryData?.having as Having),
|
||||
selectFields: isEmpty(nonEmptySelectColumns)
|
||||
? undefined
|
||||
: nonEmptySelectColumns?.map(
|
||||
(column: any): TelemetryFieldKey => ({
|
||||
name: column.name ?? column.key,
|
||||
fieldDataType:
|
||||
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||
signal: column?.signal ?? undefined,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function convertTraceOperatorToV5(
|
||||
traceOperator: Record<string, IBuilderTraceOperator>,
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(traceOperator).map(
|
||||
([queryName, traceOperatorData]): QueryEnvelope => {
|
||||
const baseSpec = createTraceOperatorBaseSpec(
|
||||
traceOperatorData,
|
||||
requestType,
|
||||
panelType,
|
||||
);
|
||||
let spec: QueryEnvelope['spec'];
|
||||
|
||||
// Skip aggregation for raw request type
|
||||
const aggregations =
|
||||
requestType === 'raw'
|
||||
? undefined
|
||||
: createAggregation(traceOperatorData, panelType);
|
||||
|
||||
spec = {
|
||||
name: queryName,
|
||||
returnSpansFrom: traceOperatorData.returnSpansFrom || '',
|
||||
...baseSpec,
|
||||
expression: traceOperatorData.expression || '',
|
||||
aggregations: aggregations as TraceAggregation[],
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'builder_trace_operator' as QueryType,
|
||||
spec,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PromQL queries to V5 format
|
||||
*/
|
||||
@ -357,14 +457,27 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
|
||||
switch (query.queryType) {
|
||||
case EQueryType.QUERY_BUILDER: {
|
||||
const { queryData: data, queryFormulas } = query.builder;
|
||||
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
|
||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||
|
||||
const filteredTraceOperator =
|
||||
queryTraceOperator && queryTraceOperator.length > 0
|
||||
? queryTraceOperator.filter((traceOperator) =>
|
||||
Boolean(traceOperator.expression.trim()),
|
||||
)
|
||||
: [];
|
||||
|
||||
const currentTraceOperator = mapQueryDataToApi(
|
||||
filteredTraceOperator,
|
||||
'queryName',
|
||||
);
|
||||
|
||||
// Combine legend maps
|
||||
legendMap = {
|
||||
...currentQueryData.newLegendMap,
|
||||
...currentFormulas.newLegendMap,
|
||||
...currentTraceOperator.newLegendMap,
|
||||
};
|
||||
|
||||
// Convert builder queries
|
||||
@ -397,8 +510,36 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const traceOperatorQueries = convertTraceOperatorToV5(
|
||||
currentTraceOperator.data,
|
||||
requestType,
|
||||
graphType,
|
||||
);
|
||||
|
||||
// const traceOperatorQueries = Object.entries(currentTraceOperator.data).map(
|
||||
// ([queryName, traceOperatorData]): QueryEnvelope => ({
|
||||
// type: 'builder_trace_operator' as const,
|
||||
// spec: {
|
||||
// name: queryName,
|
||||
// expression: traceOperatorData.expression || '',
|
||||
// legend: isEmpty(traceOperatorData.legend)
|
||||
// ? undefined
|
||||
// : traceOperatorData.legend,
|
||||
// limit: 10,
|
||||
// order: traceOperatorData.orderBy?.map(
|
||||
// // eslint-disable-next-line sonarjs/no-identical-functions
|
||||
// (order: any): OrderBy => ({
|
||||
// key: {
|
||||
// name: order.columnName,
|
||||
// },
|
||||
// direction: order.order,
|
||||
// }),
|
||||
// ),
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
// Combine both types
|
||||
queries = [...builderQueries, ...formulaQueries];
|
||||
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
|
||||
break;
|
||||
}
|
||||
case EQueryType.PROM: {
|
||||
|
||||
@ -22,6 +22,10 @@
|
||||
flex: 1;
|
||||
|
||||
position: relative;
|
||||
|
||||
.qb-trace-view-selector-container {
|
||||
padding: 12px 8px 8px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.qb-content-section {
|
||||
@ -179,7 +183,7 @@
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
margin-left: 32px;
|
||||
margin-left: 26px;
|
||||
padding-bottom: 16px;
|
||||
padding-left: 8px;
|
||||
|
||||
@ -195,8 +199,8 @@
|
||||
}
|
||||
|
||||
.formula-container {
|
||||
margin-left: 82px;
|
||||
padding: 4px 0px;
|
||||
padding: 8px;
|
||||
margin-left: 74px;
|
||||
|
||||
.ant-col {
|
||||
&::before {
|
||||
@ -331,6 +335,12 @@
|
||||
);
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
&.has-trace-operator {
|
||||
&::before {
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formula-name {
|
||||
@ -347,7 +357,7 @@
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 65px;
|
||||
height: 128px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@ -5,11 +5,13 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
|
||||
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
|
||||
import { QueryV2 } from './QueryV2/QueryV2';
|
||||
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
|
||||
|
||||
export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
config,
|
||||
@ -18,6 +20,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
queryComponents,
|
||||
isListViewPanel = false,
|
||||
showOnlyWhereClause = false,
|
||||
showTraceOperator = false,
|
||||
version,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const {
|
||||
@ -25,6 +28,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
handleSetConfig,
|
||||
addTraceOperator,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
} = useQueryBuilder();
|
||||
@ -54,6 +58,14 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
newPanelType,
|
||||
]);
|
||||
|
||||
const isMultiQueryAllowed = useMemo(
|
||||
() =>
|
||||
!showOnlyWhereClause ||
|
||||
!isListViewPanel ||
|
||||
(currentDataSource === DataSource.TRACES && showTraceOperator),
|
||||
[showOnlyWhereClause, currentDataSource, showTraceOperator, isListViewPanel],
|
||||
);
|
||||
|
||||
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
@ -97,11 +109,45 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
listViewTracesFilterConfigs,
|
||||
]);
|
||||
|
||||
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
||||
if (
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
return currentQuery.builder.queryTraceOperator[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [currentQuery.builder.queryTraceOperator]);
|
||||
|
||||
const shouldShowTraceOperator = useMemo(
|
||||
() =>
|
||||
showTraceOperator &&
|
||||
currentDataSource === DataSource.TRACES &&
|
||||
Boolean(traceOperator),
|
||||
[currentDataSource, showTraceOperator, traceOperator],
|
||||
);
|
||||
|
||||
const shouldShowFooter = useMemo(
|
||||
() =>
|
||||
(!showOnlyWhereClause && !isListViewPanel) ||
|
||||
(currentDataSource === DataSource.TRACES && showTraceOperator),
|
||||
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
|
||||
);
|
||||
|
||||
const showFormula = useMemo(() => {
|
||||
if (currentDataSource === DataSource.TRACES) {
|
||||
return !isListViewPanel;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [isListViewPanel, currentDataSource]);
|
||||
|
||||
return (
|
||||
<QueryBuilderV2Provider>
|
||||
<div className="query-builder-v2">
|
||||
<div className="qb-content-container">
|
||||
{isListViewPanel && (
|
||||
{!isMultiQueryAllowed ? (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
@ -109,15 +155,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
showTraceOperator={shouldShowTraceOperator}
|
||||
version={version}
|
||||
isAvailableToDisable={false}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isListViewPanel &&
|
||||
) : (
|
||||
currentQuery.builder.queryData.map((query, index) => (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
@ -127,13 +173,16 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version={version}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
isAvailableToDisable={false}
|
||||
showTraceOperator={shouldShowTraceOperator}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
signalSource={config?.signalSource || ''}
|
||||
/>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
@ -158,15 +207,25 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
{shouldShowFooter && (
|
||||
<QueryFooter
|
||||
showAddFormula={showFormula}
|
||||
addNewBuilderQuery={addNewBuilderQuery}
|
||||
addNewFormula={addNewFormula}
|
||||
addTraceOperator={addTraceOperator}
|
||||
showAddTraceOperator={showTraceOperator && !traceOperator}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowTraceOperator && (
|
||||
<TraceOperator
|
||||
isListViewPanel={isListViewPanel}
|
||||
traceOperator={traceOperator as IBuilderTraceOperator}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
{isMultiQueryAllowed && (
|
||||
<div className="query-names-section">
|
||||
{currentQuery.builder.queryData.map((query) => (
|
||||
<div key={query.queryName} className="query-name">
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
.query-add-ons {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-ons-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.add-ons-tabs {
|
||||
display: flex;
|
||||
|
||||
@ -144,6 +144,8 @@ function QueryAddOns({
|
||||
showReduceTo,
|
||||
panelType,
|
||||
index,
|
||||
isForTraceOperator = false,
|
||||
children,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
version: string;
|
||||
@ -151,6 +153,8 @@ function QueryAddOns({
|
||||
showReduceTo: boolean;
|
||||
panelType: PANEL_TYPES | null;
|
||||
index: number;
|
||||
isForTraceOperator?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
||||
|
||||
@ -160,6 +164,7 @@ function QueryAddOns({
|
||||
index,
|
||||
query,
|
||||
entityVersion: '',
|
||||
isForTraceOperator,
|
||||
});
|
||||
|
||||
const { handleSetQueryData } = useQueryBuilder();
|
||||
@ -486,6 +491,7 @@ function QueryAddOns({
|
||||
</Tooltip>
|
||||
))}
|
||||
</Radio.Group>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -4,7 +4,10 @@ import { Tooltip } from 'antd';
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useMemo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryAggregationSelect from './QueryAggregationSelect';
|
||||
@ -20,7 +23,7 @@ function QueryAggregationOptions({
|
||||
panelType?: string;
|
||||
onAggregationIntervalChange: (value: number) => void;
|
||||
onChange?: (value: string) => void;
|
||||
queryData: IBuilderQuery;
|
||||
queryData: IBuilderQuery | IBuilderTraceOperator;
|
||||
}): JSX.Element {
|
||||
const showAggregationInterval = useMemo(() => {
|
||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||
|
||||
@ -4,9 +4,15 @@ import { Plus, Sigma } from 'lucide-react';
|
||||
export default function QueryFooter({
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
addTraceOperator,
|
||||
showAddFormula = true,
|
||||
showAddTraceOperator = false,
|
||||
}: {
|
||||
addNewBuilderQuery: () => void;
|
||||
addNewFormula: () => void;
|
||||
addTraceOperator?: () => void;
|
||||
showAddTraceOperator: boolean;
|
||||
showAddFormula?: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="qb-footer">
|
||||
@ -22,32 +28,62 @@ export default function QueryFooter({
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="qb-add-formula">
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add New Formula
|
||||
<Typography.Link
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{' '}
|
||||
<br />
|
||||
Learn more
|
||||
</Typography.Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="add-formula-button periscope-btn secondary"
|
||||
icon={<Sigma size={16} />}
|
||||
onClick={addNewFormula}
|
||||
{showAddFormula && (
|
||||
<div className="qb-add-formula">
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add New Formula
|
||||
<Typography.Link
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{' '}
|
||||
<br />
|
||||
Learn more
|
||||
</Typography.Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Add Formula
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
className="add-formula-button periscope-btn secondary"
|
||||
icon={<Sigma size={16} />}
|
||||
onClick={addNewFormula}
|
||||
>
|
||||
Add Formula
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{showAddTraceOperator && (
|
||||
<div className="qb-add-formula">
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add Trace Matching
|
||||
<Typography.Link
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{' '}
|
||||
<br />
|
||||
Learn more
|
||||
</Typography.Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="add-formula-button periscope-btn secondary"
|
||||
icon={<Sigma size={16} />}
|
||||
onClick={() => addTraceOperator?.()}
|
||||
>
|
||||
Add Trace Matching
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
.query-where-clause-editor-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
|
||||
@ -26,9 +26,11 @@ export const QueryV2 = memo(function QueryV2({
|
||||
query,
|
||||
filterConfigs,
|
||||
isListViewPanel = false,
|
||||
showTraceOperator = false,
|
||||
version,
|
||||
showOnlyWhereClause = false,
|
||||
signalSource = '',
|
||||
isMultiQueryAllowed = false,
|
||||
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
@ -108,11 +110,15 @@ export const QueryV2 = memo(function QueryV2({
|
||||
ref={ref}
|
||||
>
|
||||
<div className="qb-content-section">
|
||||
{!showOnlyWhereClause && (
|
||||
{isMultiQueryAllowed && (
|
||||
<div className="qb-header-container">
|
||||
<div className="query-actions-container">
|
||||
<div className="query-actions-left-container">
|
||||
<QBEntityOptions
|
||||
hasTraceOperator={
|
||||
showTraceOperator ||
|
||||
(isListViewPanel && dataSource === DataSource.TRACES)
|
||||
}
|
||||
isMetricsDataSource={dataSource === DataSource.METRICS}
|
||||
showFunctions={
|
||||
(version && version === ENTITY_VERSION_V4) ||
|
||||
@ -139,7 +145,30 @@ export const QueryV2 = memo(function QueryV2({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isListViewPanel && (
|
||||
{!isCollapsed &&
|
||||
(showTraceOperator ||
|
||||
(isListViewPanel && dataSource === DataSource.TRACES)) && (
|
||||
<div className="qb-search-filter-container" style={{ flex: 1 }}>
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
signalSource={signalSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMultiQueryAllowed && (
|
||||
<Dropdown
|
||||
className="query-actions-dropdown"
|
||||
menu={{
|
||||
@ -181,28 +210,32 @@ export const QueryV2 = memo(function QueryV2({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="qb-search-filter-container">
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
signalSource={signalSource}
|
||||
/>
|
||||
</div>
|
||||
{!showTraceOperator &&
|
||||
!(isListViewPanel && dataSource === DataSource.TRACES) && (
|
||||
<div className="qb-search-filter-container">
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
signalSource={signalSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
!isListViewPanel &&
|
||||
!showTraceOperator &&
|
||||
dataSource !== DataSource.METRICS && (
|
||||
<QueryAggregation
|
||||
dataSource={dataSource}
|
||||
@ -225,7 +258,7 @@ export const QueryV2 = memo(function QueryV2({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && (
|
||||
{!showOnlyWhereClause && !isListViewPanel && !showTraceOperator && (
|
||||
<QueryAddOns
|
||||
index={index}
|
||||
query={query}
|
||||
|
||||
@ -0,0 +1,180 @@
|
||||
.qb-trace-operator {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
&.non-list-view {
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 12px;
|
||||
height: calc(100% - 48px);
|
||||
width: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&-span-source-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 24px;
|
||||
|
||||
&-query {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
&-query-name {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 2px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||
background: rgba(242, 71, 105, 0.1);
|
||||
color: var(--Sakura-400, #f56c87);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: -26px;
|
||||
height: 1px;
|
||||
width: 20px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -10px;
|
||||
transform: translateY(-50%);
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&-aggregation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&-add-ons-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&-add-ons-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 50%;
|
||||
height: 1px;
|
||||
width: 16px;
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--bg-vanilla-400);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0px 8px;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.qb-trace-operator {
|
||||
&-arrow {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
&::after {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
&.non-list-view {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&-add-ons-input {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
|
||||
.label {
|
||||
color: var(--bg-ink-500) !important;
|
||||
border-right: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
|
||||
import './TraceOperator.styles.scss';
|
||||
|
||||
import { Button, Select, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryAddOns from '../QueryAddOns/QueryAddOns';
|
||||
import QueryAggregation from '../QueryAggregation/QueryAggregation';
|
||||
|
||||
export default function TraceOperator({
|
||||
traceOperator,
|
||||
isListViewPanel = false,
|
||||
}: {
|
||||
traceOperator: IBuilderTraceOperator;
|
||||
isListViewPanel?: boolean;
|
||||
}): JSX.Element {
|
||||
const { panelType, currentQuery, removeTraceOperator } = useQueryBuilder();
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: traceOperator,
|
||||
entityVersion: '',
|
||||
isForTraceOperator: true,
|
||||
});
|
||||
|
||||
const handleTraceOperatorChange = useCallback(
|
||||
(traceOperatorExpression: string) => {
|
||||
handleChangeQueryData('expression', traceOperatorExpression);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeAggregateEvery = useCallback(
|
||||
(value: IBuilderQuery['stepInterval']) => {
|
||||
handleChangeQueryData('stepInterval', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeAggregation = useCallback(
|
||||
(value: string) => {
|
||||
handleChangeQueryData('aggregations', [
|
||||
{
|
||||
expression: value,
|
||||
},
|
||||
]);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeSpanSource = useCallback(
|
||||
(value: string) => {
|
||||
handleChangeQueryData('returnSpansFrom', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const defaultSpanSource = useMemo(
|
||||
() =>
|
||||
traceOperator.returnSpansFrom ||
|
||||
currentQuery.builder.queryData[0].queryName ||
|
||||
'',
|
||||
[currentQuery.builder.queryData, traceOperator?.returnSpansFrom],
|
||||
);
|
||||
|
||||
const spanSourceOptions = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.map((query) => ({
|
||||
value: query.queryName,
|
||||
label: (
|
||||
<div className="qb-trace-operator-span-source-label">
|
||||
<span className="qb-trace-operator-span-source-label-query">Query</span>
|
||||
<p className="qb-trace-operator-span-source-label-query-name">
|
||||
{query.queryName}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
[currentQuery.builder.queryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
|
||||
<div className="qb-trace-operator-container">
|
||||
<InputWithLabel
|
||||
className={cx(
|
||||
'qb-trace-operator-input',
|
||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||
)}
|
||||
initialValue={traceOperator?.expression || ''}
|
||||
label="TRACES MATCHING"
|
||||
placeholder="Add condition..."
|
||||
type="text"
|
||||
onChange={handleTraceOperatorChange}
|
||||
/>
|
||||
{!isListViewPanel && (
|
||||
<div className="qb-trace-operator-aggregation-container">
|
||||
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
|
||||
<QueryAggregation
|
||||
dataSource={DataSource.TRACES}
|
||||
key={`query-search-${traceOperator.queryName}`}
|
||||
panelType={panelType || undefined}
|
||||
onAggregationIntervalChange={handleChangeAggregateEvery}
|
||||
onChange={handleChangeAggregation}
|
||||
queryData={traceOperator}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
'qb-trace-operator-add-ons-container',
|
||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||
)}
|
||||
>
|
||||
<QueryAddOns
|
||||
index={0}
|
||||
query={traceOperator}
|
||||
version="v3"
|
||||
isForTraceOperator
|
||||
isListViewPanel={false}
|
||||
showReduceTo={false}
|
||||
panelType={panelType}
|
||||
>
|
||||
<div className="qb-trace-operator-add-ons-input">
|
||||
<Typography.Text className="label">Using spans from</Typography.Text>
|
||||
<Select
|
||||
bordered={false}
|
||||
defaultValue={defaultSpanSource}
|
||||
style={{ minWidth: 120 }}
|
||||
onChange={handleChangeSpanSource}
|
||||
options={spanSourceOptions}
|
||||
listItemHeight={24}
|
||||
/>
|
||||
</div>
|
||||
</QueryAddOns>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip title="Remove Trace Operator" placement="topLeft">
|
||||
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -17,6 +17,19 @@
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.view-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
&:hover {
|
||||
|
||||
@ -6,6 +6,7 @@ import { RadioChangeEvent } from 'antd/es/radio';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface SignozRadioGroupProps {
|
||||
@ -37,7 +38,10 @@ function SignozRadioGroup({
|
||||
value={option.value}
|
||||
className={value === option.value ? 'selected_view tab' : 'tab'}
|
||||
>
|
||||
{option.label}
|
||||
<div className="view-title-container">
|
||||
{option.icon && <div className="icon-container">{option.icon}</div>}
|
||||
{option.label}
|
||||
</div>
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
HavingForm,
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
Query,
|
||||
@ -50,6 +51,8 @@ import {
|
||||
export const MAX_FORMULAS = 20;
|
||||
export const MAX_QUERIES = 26;
|
||||
|
||||
export const TRACE_OPERATOR_QUERY_NAME = 'T1';
|
||||
|
||||
export const idDivider = '--';
|
||||
export const selectValueDivider = '__';
|
||||
|
||||
@ -265,6 +268,11 @@ export const initialFormulaBuilderFormValues: IBuilderFormula = {
|
||||
legend: '',
|
||||
};
|
||||
|
||||
export const initialQueryBuilderFormTraceOperatorValues: IBuilderTraceOperator = {
|
||||
...initialQueryBuilderFormTracesValues,
|
||||
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||
};
|
||||
|
||||
export const initialQueryPromQLData: IPromQLQuery = {
|
||||
name: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||
query: '',
|
||||
@ -282,6 +290,7 @@ export const initialClickHouseData: IClickHouseQuery = {
|
||||
export const initialQueryBuilderData: QueryBuilderData = {
|
||||
queryData: [initialQueryBuilderFormValues],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
};
|
||||
|
||||
export const initialSingleQueryMap: Record<
|
||||
|
||||
@ -54,6 +54,7 @@ function QuerySection({
|
||||
queryVariant: 'static',
|
||||
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
||||
}}
|
||||
showTraceOperator={alertType === AlertTypes.TRACES_BASED_ALERT}
|
||||
showFunctions={
|
||||
(alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||
alertDef.version === ENTITY_VERSION_V4) ||
|
||||
|
||||
@ -150,6 +150,7 @@ function FormAlertRules({
|
||||
|
||||
const queryOptions = useMemo(() => {
|
||||
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
|
||||
// TODO: Filter out queries who are used in trace operator
|
||||
[EQueryType.QUERY_BUILDER]: () => [
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
|
||||
|
||||
@ -30,5 +30,14 @@ export type QueryBuilderProps = {
|
||||
isListViewPanel?: boolean;
|
||||
showFunctions?: boolean;
|
||||
showOnlyWhereClause?: boolean;
|
||||
showOnlyTraceOperator?: boolean;
|
||||
showTraceViewSelector?: boolean;
|
||||
showTraceOperator?: boolean;
|
||||
version: string;
|
||||
onChangeTraceView?: (view: TraceView) => void;
|
||||
};
|
||||
|
||||
export enum TraceView {
|
||||
SPANS = 'spans',
|
||||
TRACES = 'traces',
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ interface QBEntityOptionsProps {
|
||||
showCloneOption?: boolean;
|
||||
isListViewPanel?: boolean;
|
||||
index?: number;
|
||||
hasTraceOperator?: boolean;
|
||||
queryVariant?: 'dropdown' | 'static';
|
||||
onChangeDataSource?: (value: DataSource) => void;
|
||||
}
|
||||
@ -61,6 +62,7 @@ export default function QBEntityOptions({
|
||||
onCloneQuery,
|
||||
index,
|
||||
queryVariant,
|
||||
hasTraceOperator = false,
|
||||
onChangeDataSource,
|
||||
}: QBEntityOptionsProps): JSX.Element {
|
||||
const handleCloneEntity = (): void => {
|
||||
@ -97,7 +99,7 @@ export default function QBEntityOptions({
|
||||
value="query-builder"
|
||||
className="periscope-btn visibility-toggle"
|
||||
onClick={onToggleVisibility}
|
||||
disabled={isListViewPanel}
|
||||
disabled={isListViewPanel && query?.dataSource !== DataSource.TRACES}
|
||||
>
|
||||
{entityData.disabled ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</Button>
|
||||
@ -115,6 +117,7 @@ export default function QBEntityOptions({
|
||||
className={cx(
|
||||
'periscope-btn',
|
||||
entityType === 'query' ? 'query-name' : 'formula-name',
|
||||
hasTraceOperator && 'has-trace-operator',
|
||||
isLogsExplorerPage && lastUsedQuery === index ? 'sync-btn' : '',
|
||||
)}
|
||||
>
|
||||
|
||||
@ -11,5 +11,7 @@ export type QueryProps = {
|
||||
version: string;
|
||||
showSpanScopeSelector?: boolean;
|
||||
showOnlyWhereClause?: boolean;
|
||||
showTraceOperator?: boolean;
|
||||
signalSource?: string;
|
||||
isMultiQueryAllowed?: boolean;
|
||||
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;
|
||||
|
||||
@ -37,11 +37,15 @@ function QuerySection(): JSX.Element {
|
||||
};
|
||||
}, [panelTypes, renderOrderBy]);
|
||||
|
||||
const isListViewPanel = useMemo(
|
||||
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
|
||||
[panelTypes],
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryBuilderV2
|
||||
isListViewPanel={
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||
}
|
||||
isListViewPanel={isListViewPanel}
|
||||
showTraceOperator
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
queryComponents={queryComponents}
|
||||
panelType={panelTypes}
|
||||
|
||||
@ -54,9 +54,11 @@ export const useQueryOperations: UseQueryOperations = ({
|
||||
formula,
|
||||
isListViewPanel = false,
|
||||
entityVersion,
|
||||
isForTraceOperator = false,
|
||||
}) => {
|
||||
const {
|
||||
handleSetQueryData,
|
||||
handleSetTraceOperatorData,
|
||||
handleSetFormulaData,
|
||||
removeQueryBuilderEntityByIndex,
|
||||
panelType,
|
||||
@ -400,9 +402,19 @@ export const useQueryOperations: UseQueryOperations = ({
|
||||
: value,
|
||||
};
|
||||
|
||||
handleSetQueryData(index, newQuery);
|
||||
if (isForTraceOperator) {
|
||||
handleSetTraceOperatorData(index, newQuery);
|
||||
} else {
|
||||
handleSetQueryData(index, newQuery);
|
||||
}
|
||||
},
|
||||
[query, index, handleSetQueryData],
|
||||
[
|
||||
query,
|
||||
index,
|
||||
handleSetQueryData,
|
||||
handleSetTraceOperatorData,
|
||||
isForTraceOperator,
|
||||
],
|
||||
);
|
||||
|
||||
const handleChangeFormulaData: HandleChangeFormulaData = useCallback(
|
||||
|
||||
@ -53,7 +53,6 @@ function TracesExplorer(): JSX.Element {
|
||||
handleRunQuery,
|
||||
stagedQuery,
|
||||
handleSetConfig,
|
||||
updateQueriesData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
@ -112,48 +111,14 @@ function TracesExplorer(): JSX.Element {
|
||||
handleSetConfig(PANEL_TYPES.LIST, DataSource.TRACES);
|
||||
}
|
||||
|
||||
if (view === ExplorerViews.LIST) {
|
||||
if (
|
||||
selectedView !== ExplorerViews.LIST &&
|
||||
currentQuery?.builder?.queryData?.[0]
|
||||
) {
|
||||
const filterToRetain = currentQuery.builder.queryData[0].filter;
|
||||
|
||||
const newDefaultQuery = updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
);
|
||||
|
||||
const newListQuery = updateQueriesData(
|
||||
newDefaultQuery,
|
||||
'queryData',
|
||||
(item, index) => {
|
||||
if (index === 0) {
|
||||
return { ...item, filter: filterToRetain };
|
||||
}
|
||||
return item;
|
||||
},
|
||||
);
|
||||
setDefaultQuery(newListQuery);
|
||||
}
|
||||
setShouldReset(true);
|
||||
}
|
||||
// TODO: remove formula when switching to List view
|
||||
|
||||
setSelectedView(view);
|
||||
handleExplorerTabChange(
|
||||
view === ExplorerViews.TIMESERIES ? PANEL_TYPES.TIME_SERIES : view,
|
||||
);
|
||||
},
|
||||
[
|
||||
handleSetConfig,
|
||||
handleExplorerTabChange,
|
||||
selectedView,
|
||||
currentQuery,
|
||||
updateAllQueriesOperators,
|
||||
updateQueriesData,
|
||||
setSelectedView,
|
||||
],
|
||||
[handleSetConfig, handleExplorerTabChange, selectedView, setSelectedView],
|
||||
);
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
initialClickHouseData,
|
||||
initialFormulaBuilderFormValues,
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormTraceOperatorValues,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryPromQLData,
|
||||
initialQueryState,
|
||||
@ -14,6 +15,7 @@ import {
|
||||
MAX_FORMULAS,
|
||||
MAX_QUERIES,
|
||||
PANEL_TYPES,
|
||||
TRACE_OPERATOR_QUERY_NAME,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
@ -47,6 +49,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
Query,
|
||||
@ -75,14 +78,18 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
isEnabledQuery: false,
|
||||
handleSetQueryData: () => {},
|
||||
handleSetTraceOperatorData: () => {},
|
||||
handleSetFormulaData: () => {},
|
||||
handleSetQueryItemData: () => {},
|
||||
handleSetConfig: () => {},
|
||||
removeQueryBuilderEntityByIndex: () => {},
|
||||
removeAllQueryBuilderEntities: () => {},
|
||||
removeQueryTypeItemByIndex: () => {},
|
||||
addNewBuilderQuery: () => {},
|
||||
cloneQuery: () => {},
|
||||
addNewFormula: () => {},
|
||||
addTraceOperator: () => {},
|
||||
removeTraceOperator: () => {},
|
||||
addNewQueryItem: () => {},
|
||||
redirectWithQueryBuilderData: () => {},
|
||||
handleRunQuery: () => {},
|
||||
@ -173,6 +180,10 @@ export function QueryBuilderProvider({
|
||||
...initialFormulaBuilderFormValues,
|
||||
...item,
|
||||
})),
|
||||
queryTraceOperator: query.builder.queryTraceOperator?.map((item) => ({
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
...item,
|
||||
})),
|
||||
};
|
||||
|
||||
const setupedQueryData = builder.queryData.map((item) => {
|
||||
@ -385,8 +396,11 @@ export function QueryBuilderProvider({
|
||||
const removeQueryBuilderEntityByIndex = useCallback(
|
||||
(type: keyof QueryBuilderData, index: number) => {
|
||||
setCurrentQuery((prevState) => {
|
||||
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
|
||||
prevState.builder[type];
|
||||
const currentArray: (
|
||||
| IBuilderQuery
|
||||
| IBuilderFormula
|
||||
| IBuilderTraceOperator
|
||||
)[] = prevState.builder[type];
|
||||
|
||||
const filteredArray = currentArray.filter((_, i) => index !== i);
|
||||
|
||||
@ -400,8 +414,11 @@ export function QueryBuilderProvider({
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
|
||||
prevState.builder[type];
|
||||
const currentArray: (
|
||||
| IBuilderQuery
|
||||
| IBuilderFormula
|
||||
| IBuilderTraceOperator
|
||||
)[] = prevState.builder[type];
|
||||
|
||||
const filteredArray = currentArray.filter((_, i) => index !== i);
|
||||
|
||||
@ -417,6 +434,20 @@ export function QueryBuilderProvider({
|
||||
[],
|
||||
);
|
||||
|
||||
const removeAllQueryBuilderEntities = useCallback(
|
||||
(type: keyof QueryBuilderData) => {
|
||||
setCurrentQuery((prevState) => ({
|
||||
...prevState,
|
||||
builder: { ...prevState.builder, [type]: [] },
|
||||
}));
|
||||
setSupersetQuery((prevState) => ({
|
||||
...prevState,
|
||||
builder: { ...prevState.builder, [type]: [] },
|
||||
}));
|
||||
},
|
||||
[setCurrentQuery, setSupersetQuery],
|
||||
);
|
||||
|
||||
const removeQueryTypeItemByIndex = useCallback(
|
||||
(type: EQueryType.PROM | EQueryType.CLICKHOUSE, index: number) => {
|
||||
setCurrentQuery((prevState) => {
|
||||
@ -639,6 +670,68 @@ export function QueryBuilderProvider({
|
||||
});
|
||||
}, [createNewBuilderFormula]);
|
||||
|
||||
const addTraceOperator = useCallback((expression = '') => {
|
||||
const trimmed = (expression || '').trim();
|
||||
|
||||
setCurrentQuery((prevState) => {
|
||||
const existing = prevState.builder.queryTraceOperator?.[0] || null;
|
||||
const updated: IBuilderTraceOperator = existing
|
||||
? { ...existing, expression: trimmed }
|
||||
: {
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||
expression: trimmed,
|
||||
};
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
// enforce single trace operator and replace only expression
|
||||
queryTraceOperator: [updated],
|
||||
},
|
||||
};
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
const existing = prevState.builder.queryTraceOperator?.[0] || null;
|
||||
const updated: IBuilderTraceOperator = existing
|
||||
? { ...existing, expression: trimmed }
|
||||
: {
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||
expression: trimmed,
|
||||
};
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
// enforce single trace operator and replace only expression
|
||||
queryTraceOperator: [updated],
|
||||
},
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeTraceOperator = useCallback(() => {
|
||||
setCurrentQuery((prevState) => ({
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
}));
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => ({
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const updateQueryBuilderData: <T>(
|
||||
arr: T[],
|
||||
index: number,
|
||||
@ -745,6 +838,44 @@ export function QueryBuilderProvider({
|
||||
},
|
||||
[updateQueryBuilderData, updateSuperSetQueryBuilderData],
|
||||
);
|
||||
|
||||
const handleSetTraceOperatorData = useCallback(
|
||||
(index: number, traceOperatorData: IBuilderTraceOperator): void => {
|
||||
setCurrentQuery((prevState) => {
|
||||
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
|
||||
prevState.builder.queryTraceOperator,
|
||||
index,
|
||||
traceOperatorData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryTraceOperator: updatedTraceOperatorBuilderData,
|
||||
},
|
||||
};
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
|
||||
prevState.builder.queryTraceOperator,
|
||||
index,
|
||||
traceOperatorData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryTraceOperator: updatedTraceOperatorBuilderData,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[updateQueryBuilderData],
|
||||
);
|
||||
|
||||
const handleSetFormulaData = useCallback(
|
||||
(index: number, formulaData: IBuilderFormula): void => {
|
||||
setCurrentQuery((prevState) => {
|
||||
@ -1045,14 +1176,18 @@ export function QueryBuilderProvider({
|
||||
panelType,
|
||||
isEnabledQuery,
|
||||
handleSetQueryData,
|
||||
handleSetTraceOperatorData,
|
||||
handleSetFormulaData,
|
||||
handleSetQueryItemData,
|
||||
handleSetConfig,
|
||||
removeQueryBuilderEntityByIndex,
|
||||
removeQueryTypeItemByIndex,
|
||||
removeAllQueryBuilderEntities,
|
||||
cloneQuery,
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
addTraceOperator,
|
||||
removeTraceOperator,
|
||||
addNewQueryItem,
|
||||
redirectWithQueryBuilderData,
|
||||
handleRunQuery,
|
||||
@ -1073,14 +1208,18 @@ export function QueryBuilderProvider({
|
||||
panelType,
|
||||
isEnabledQuery,
|
||||
handleSetQueryData,
|
||||
handleSetTraceOperatorData,
|
||||
handleSetFormulaData,
|
||||
handleSetQueryItemData,
|
||||
handleSetConfig,
|
||||
removeQueryBuilderEntityByIndex,
|
||||
removeQueryTypeItemByIndex,
|
||||
removeAllQueryBuilderEntities,
|
||||
cloneQuery,
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
addTraceOperator,
|
||||
removeTraceOperator,
|
||||
addNewQueryItem,
|
||||
redirectWithQueryBuilderData,
|
||||
handleRunQuery,
|
||||
|
||||
@ -29,6 +29,10 @@ export interface IBuilderFormula {
|
||||
orderBy?: OrderByPayload[];
|
||||
}
|
||||
|
||||
export type IBuilderTraceOperator = IBuilderQuery & {
|
||||
returnSpansFrom?: string;
|
||||
};
|
||||
|
||||
export interface TagFilterItem {
|
||||
id: string;
|
||||
key?: BaseAutocompleteData;
|
||||
@ -124,6 +128,7 @@ export type BuilderQueryDataResourse = Record<
|
||||
export type MapData =
|
||||
| IBuilderQuery
|
||||
| IBuilderFormula
|
||||
| IBuilderTraceOperator
|
||||
| IClickHouseQuery
|
||||
| IPromQLQuery;
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ export type RequestType =
|
||||
|
||||
export type QueryType =
|
||||
| 'builder_query'
|
||||
| 'builder_trace_operator'
|
||||
| 'builder_formula'
|
||||
| 'builder_sub_query'
|
||||
| 'builder_join'
|
||||
|
||||
@ -4,6 +4,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
BaseBuilderQuery,
|
||||
@ -18,6 +19,7 @@ import { SelectOption } from './select';
|
||||
|
||||
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
|
||||
Pick<QueryBuilderProps, 'filterConfigs'> & {
|
||||
isForTraceOperator?: boolean;
|
||||
formula?: IBuilderFormula;
|
||||
isListViewPanel?: boolean;
|
||||
entityVersion: string;
|
||||
@ -32,6 +34,14 @@ export type HandleChangeQueryData<T = IBuilderQuery> = <
|
||||
value: Value,
|
||||
) => void;
|
||||
|
||||
export type HandleChangeTraceOperatorData<T = IBuilderTraceOperator> = <
|
||||
Key extends keyof T,
|
||||
Value extends T[Key]
|
||||
>(
|
||||
key: Key,
|
||||
value: Value,
|
||||
) => void;
|
||||
|
||||
// Legacy version for backward compatibility
|
||||
export type HandleChangeQueryDataLegacy = HandleChangeQueryData<IBuilderQuery>;
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { Dispatch, SetStateAction } from 'react';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
Query,
|
||||
@ -222,6 +223,7 @@ export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min';
|
||||
export type QueryBuilderData = {
|
||||
queryData: IBuilderQuery[];
|
||||
queryFormulas: IBuilderFormula[];
|
||||
queryTraceOperator: IBuilderTraceOperator[];
|
||||
};
|
||||
|
||||
export type QueryBuilderContextType = {
|
||||
@ -235,6 +237,10 @@ export type QueryBuilderContextType = {
|
||||
panelType: PANEL_TYPES | null;
|
||||
isEnabledQuery: boolean;
|
||||
handleSetQueryData: (index: number, queryData: IBuilderQuery) => void;
|
||||
handleSetTraceOperatorData: (
|
||||
index: number,
|
||||
traceOperatorData: IBuilderTraceOperator,
|
||||
) => void;
|
||||
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
|
||||
handleSetQueryItemData: (
|
||||
index: number,
|
||||
@ -249,12 +255,15 @@ export type QueryBuilderContextType = {
|
||||
type: keyof QueryBuilderData,
|
||||
index: number,
|
||||
) => void;
|
||||
removeAllQueryBuilderEntities: (type: keyof QueryBuilderData) => void;
|
||||
removeQueryTypeItemByIndex: (
|
||||
type: EQueryType.PROM | EQueryType.CLICKHOUSE,
|
||||
index: number,
|
||||
) => void;
|
||||
addNewBuilderQuery: () => void;
|
||||
addNewFormula: () => void;
|
||||
removeTraceOperator: () => void;
|
||||
addTraceOperator: (expression?: string) => void;
|
||||
cloneQuery: (type: string, query: IBuilderQuery) => void;
|
||||
addNewQueryItem: (type: EQueryType.PROM | EQueryType.CLICKHOUSE) => void;
|
||||
redirectWithQueryBuilderData: (
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@ -97,7 +98,12 @@ func (a *APIKey) Wrap(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("auth_type", "api_key")
|
||||
comment.Set("user_id", claims.UserID)
|
||||
comment.Set("org_id", claims.OrgID)
|
||||
|
||||
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@ -50,7 +51,12 @@ func (a *Auth) Wrap(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("auth_type", "jwt")
|
||||
comment.Set("user_id", claims.UserID)
|
||||
comment.Set("org_id", claims.OrgID)
|
||||
|
||||
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
24
pkg/http/middleware/comment.go
Normal file
24
pkg/http/middleware/comment.go
Normal file
@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
)
|
||||
|
||||
type Comment struct{}
|
||||
|
||||
func NewComment() *Comment {
|
||||
return &Comment{}
|
||||
}
|
||||
|
||||
func (middleware *Comment) Wrap(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
comment := ctxtypes.CommentFromContext(req.Context())
|
||||
comment.Merge(ctxtypes.CommentFromHTTPRequest(req))
|
||||
|
||||
req = req.WithContext(ctxtypes.NewContextWithComment(req.Context(), comment))
|
||||
next.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
@ -2,16 +2,11 @@ package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/gorilla/mux"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
)
|
||||
@ -55,9 +50,6 @@ func (middleware *Logging) Wrap(next http.Handler) http.Handler {
|
||||
string(semconv.HTTPRouteKey), path,
|
||||
}
|
||||
|
||||
logCommentKVs := middleware.getLogCommentKVs(req)
|
||||
req = req.WithContext(context.WithValue(req.Context(), common.LogCommentKey, logCommentKVs))
|
||||
|
||||
badResponseBuffer := new(bytes.Buffer)
|
||||
writer := newBadResponseLoggingWriter(rw, badResponseBuffer)
|
||||
next.ServeHTTP(writer, req)
|
||||
@ -85,67 +77,3 @@ func (middleware *Logging) Wrap(next http.Handler) http.Handler {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (middleware *Logging) getLogCommentKVs(r *http.Request) map[string]string {
|
||||
referrer := r.Header.Get("Referer")
|
||||
|
||||
var path, dashboardID, alertID, page, client, viewName, tab string
|
||||
|
||||
if referrer != "" {
|
||||
referrerURL, _ := url.Parse(referrer)
|
||||
client = "browser"
|
||||
path = referrerURL.Path
|
||||
|
||||
if strings.Contains(path, "/dashboard") {
|
||||
// Split the path into segments
|
||||
pathSegments := strings.Split(referrerURL.Path, "/")
|
||||
// The dashboard ID should be the segment after "/dashboard/"
|
||||
// Loop through pathSegments to find "dashboard" and then take the next segment as the ID
|
||||
for i, segment := range pathSegments {
|
||||
if segment == "dashboard" && i < len(pathSegments)-1 {
|
||||
// Return the next segment, which should be the dashboard ID
|
||||
dashboardID = pathSegments[i+1]
|
||||
}
|
||||
}
|
||||
page = "dashboards"
|
||||
} else if strings.Contains(path, "/alerts") {
|
||||
urlParams := referrerURL.Query()
|
||||
alertID = urlParams.Get("ruleId")
|
||||
page = "alerts"
|
||||
} else if strings.Contains(path, "logs") && strings.Contains(path, "explorer") {
|
||||
page = "logs-explorer"
|
||||
viewName = referrerURL.Query().Get("viewName")
|
||||
} else if strings.Contains(path, "/trace") || strings.Contains(path, "traces-explorer") {
|
||||
page = "traces-explorer"
|
||||
viewName = referrerURL.Query().Get("viewName")
|
||||
} else if strings.Contains(path, "/services") {
|
||||
page = "services"
|
||||
tab = referrerURL.Query().Get("tab")
|
||||
if tab == "" {
|
||||
tab = "OVER_METRICS"
|
||||
}
|
||||
} else if strings.Contains(path, "/metrics") {
|
||||
page = "metrics-explorer"
|
||||
}
|
||||
} else {
|
||||
client = "api"
|
||||
}
|
||||
|
||||
var email string
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err == nil {
|
||||
email = claims.Email
|
||||
}
|
||||
|
||||
kvs := map[string]string{
|
||||
"path": path,
|
||||
"dashboardID": dashboardID,
|
||||
"alertID": alertID,
|
||||
"source": page,
|
||||
"client": client,
|
||||
"viewName": viewName,
|
||||
"servicesTab": tab,
|
||||
"email": email,
|
||||
}
|
||||
return kvs
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
@ -12,6 +11,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/variables"
|
||||
@ -166,49 +166,9 @@ func (a *API) logEvent(ctx context.Context, referrer string, event *qbtypes.QBEv
|
||||
return
|
||||
}
|
||||
|
||||
properties["referrer"] = referrer
|
||||
|
||||
logsExplorerMatched, _ := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
|
||||
traceExplorerMatched, _ := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
|
||||
metricsExplorerMatched, _ := regexp.MatchString(`/metrics-explorer/explorer(?:\?.*)?$`, referrer)
|
||||
dashboardMatched, _ := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
|
||||
alertMatched, _ := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
|
||||
|
||||
switch {
|
||||
case dashboardMatched:
|
||||
properties["module_name"] = "dashboard"
|
||||
case alertMatched:
|
||||
properties["module_name"] = "rule"
|
||||
case metricsExplorerMatched:
|
||||
properties["module_name"] = "metrics-explorer"
|
||||
case logsExplorerMatched:
|
||||
properties["module_name"] = "logs-explorer"
|
||||
case traceExplorerMatched:
|
||||
properties["module_name"] = "traces-explorer"
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if dashboardMatched {
|
||||
if dashboardIDRegex, err := regexp.Compile(`/dashboard/([a-f0-9\-]+)/`); err == nil {
|
||||
if matches := dashboardIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
properties["dashboard_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
if widgetIDRegex, err := regexp.Compile(`widgetId=([a-f0-9\-]+)`); err == nil {
|
||||
if matches := widgetIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
properties["widget_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if alertMatched {
|
||||
if alertIDRegex, err := regexp.Compile(`ruleId=(\d+)`); err == nil {
|
||||
if matches := alertIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
properties["rule_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
comments := ctxtypes.CommentFromContext(ctx).Map()
|
||||
for key, value := range comments {
|
||||
properties[key] = value
|
||||
}
|
||||
|
||||
if !event.HasData {
|
||||
|
||||
@ -3640,28 +3640,8 @@ func readRowsForTimeSeriesResult(rows driver.Rows, vars []interface{}, columnNam
|
||||
return seriesList, getPersonalisedError(rows.Err())
|
||||
}
|
||||
|
||||
func logCommentKVs(ctx context.Context) map[string]string {
|
||||
kv := ctx.Value(common.LogCommentKey)
|
||||
if kv == nil {
|
||||
return nil
|
||||
}
|
||||
logCommentKVs, ok := kv.(map[string]string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return logCommentKVs
|
||||
}
|
||||
|
||||
// GetTimeSeriesResultV3 runs the query and returns list of time series
|
||||
func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query string) ([]*v3.Series, error) {
|
||||
|
||||
ctxArgs := map[string]interface{}{"query": query}
|
||||
for k, v := range logCommentKVs(ctx) {
|
||||
ctxArgs[k] = v
|
||||
}
|
||||
|
||||
defer utils.Elapsed("GetTimeSeriesResultV3", ctxArgs)()
|
||||
|
||||
// Hook up query progress reporting if requested.
|
||||
queryId := ctx.Value("queryId")
|
||||
if queryId != nil {
|
||||
@ -3725,20 +3705,12 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri
|
||||
|
||||
// GetListResultV3 runs the query and returns list of rows
|
||||
func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([]*v3.Row, error) {
|
||||
|
||||
ctxArgs := map[string]interface{}{"query": query}
|
||||
for k, v := range logCommentKVs(ctx) {
|
||||
ctxArgs[k] = v
|
||||
}
|
||||
|
||||
defer utils.Elapsed("GetListResultV3", ctxArgs)()
|
||||
|
||||
rows, err := r.db.Query(ctx, query)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("error while reading time series result", zap.Error(err))
|
||||
return nil, errors.New(err.Error())
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var (
|
||||
|
||||
@ -220,6 +220,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
r.Use(middleware.NewComment().Wrap)
|
||||
|
||||
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())
|
||||
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
package common
|
||||
|
||||
type LogCommentContextKeyType string
|
||||
|
||||
const LogCommentKey LogCommentContextKeyType = "logComment"
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
@ -369,12 +369,10 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
|
||||
rule.SetEvaluationTimestamp(t)
|
||||
}(time.Now())
|
||||
|
||||
kvs := map[string]string{
|
||||
"alertID": rule.ID(),
|
||||
"source": "alerts",
|
||||
"client": "query-service",
|
||||
}
|
||||
ctx = context.WithValue(ctx, common.LogCommentKey, kvs)
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("rule_id", rule.ID())
|
||||
comment.Set("auth_type", "internal")
|
||||
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
||||
|
||||
_, err := rule.Eval(ctx, ts)
|
||||
if err != nil {
|
||||
|
||||
@ -7,8 +7,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
@ -352,12 +352,10 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
|
||||
rule.SetEvaluationTimestamp(t)
|
||||
}(time.Now())
|
||||
|
||||
kvs := map[string]string{
|
||||
"alertID": rule.ID(),
|
||||
"source": "alerts",
|
||||
"client": "query-service",
|
||||
}
|
||||
ctx = context.WithValue(ctx, common.LogCommentKey, kvs)
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("rule_id", rule.ID())
|
||||
comment.Set("auth_type", "internal")
|
||||
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
||||
|
||||
_, err := rule.Eval(ctx, ts)
|
||||
if err != nil {
|
||||
|
||||
@ -2,13 +2,12 @@ package telemetrystorehook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
@ -32,11 +31,7 @@ func NewSettings(ctx context.Context, providerSettings factory.ProviderSettings,
|
||||
func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent) context.Context {
|
||||
settings := clickhouse.Settings{}
|
||||
|
||||
// Apply default settings
|
||||
logComment := h.getLogComment(ctx)
|
||||
if logComment != "" {
|
||||
settings["log_comment"] = logComment
|
||||
}
|
||||
settings["log_comment"] = ctxtypes.CommentFromContext(ctx).String()
|
||||
|
||||
if ctx.Value("enforce_max_result_rows") != nil {
|
||||
settings["max_result_rows"] = h.settings.MaxResultRows
|
||||
@ -91,22 +86,4 @@ func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (h *provider) AfterQuery(ctx context.Context, event *telemetrystore.QueryEvent) {
|
||||
}
|
||||
|
||||
func (h *provider) getLogComment(ctx context.Context) string {
|
||||
// Get the key-value pairs from context for log comment
|
||||
kv := ctx.Value(common.LogCommentKey)
|
||||
if kv == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
logCommentKVs, ok := kv.(map[string]string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
logComment, _ := json.Marshal(logCommentKVs)
|
||||
|
||||
return string(logComment)
|
||||
}
|
||||
func (h *provider) AfterQuery(ctx context.Context, event *telemetrystore.QueryEvent) {}
|
||||
|
||||
163
pkg/types/ctxtypes/comment.go
Normal file
163
pkg/types/ctxtypes/comment.go
Normal file
@ -0,0 +1,163 @@
|
||||
package ctxtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
logsExplorerRegex = regexp.MustCompile(`/logs/logs-explorer(?:\?.*)?$`)
|
||||
traceExplorerRegex = regexp.MustCompile(`/traces-explorer(?:\?.*)?$`)
|
||||
metricsExplorerRegex = regexp.MustCompile(`/metrics-explorer/explorer(?:\?.*)?$`)
|
||||
dashboardRegex = regexp.MustCompile(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`)
|
||||
dashboardIDRegex = regexp.MustCompile(`/dashboard/([a-f0-9\-]+)/`)
|
||||
widgetIDRegex = regexp.MustCompile(`widgetId=([a-f0-9\-]+)`)
|
||||
ruleRegex = regexp.MustCompile(`/alerts/(new|edit)(?:\?.*)?$`)
|
||||
ruleIDRegex = regexp.MustCompile(`ruleId=(\d+)`)
|
||||
)
|
||||
|
||||
type commentCtxKey struct{}
|
||||
|
||||
type Comment struct {
|
||||
vals map[string]string
|
||||
mtx sync.RWMutex
|
||||
}
|
||||
|
||||
func NewContextWithComment(ctx context.Context, comment *Comment) context.Context {
|
||||
return context.WithValue(ctx, commentCtxKey{}, comment)
|
||||
}
|
||||
|
||||
func CommentFromContext(ctx context.Context) *Comment {
|
||||
comment, ok := ctx.Value(commentCtxKey{}).(*Comment)
|
||||
if !ok {
|
||||
return NewComment()
|
||||
}
|
||||
|
||||
// Return a deep copy of the comment to prevent mutations from affecting the original
|
||||
copy := NewComment()
|
||||
copy.Merge(comment.Map())
|
||||
return copy
|
||||
}
|
||||
|
||||
func CommentFromHTTPRequest(req *http.Request) map[string]string {
|
||||
comments := map[string]string{}
|
||||
|
||||
referrer := req.Header.Get("Referer")
|
||||
if referrer == "" {
|
||||
return comments
|
||||
}
|
||||
|
||||
referrerURL, err := url.Parse(referrer)
|
||||
if err != nil {
|
||||
return comments
|
||||
}
|
||||
|
||||
logsExplorerMatched := logsExplorerRegex.MatchString(referrer)
|
||||
traceExplorerMatched := traceExplorerRegex.MatchString(referrer)
|
||||
metricsExplorerMatched := metricsExplorerRegex.MatchString(referrer)
|
||||
dashboardMatched := dashboardRegex.MatchString(referrer)
|
||||
ruleMatched := ruleRegex.MatchString(referrer)
|
||||
|
||||
switch {
|
||||
case dashboardMatched:
|
||||
comments["module_name"] = "dashboard"
|
||||
case ruleMatched:
|
||||
comments["module_name"] = "rule"
|
||||
case metricsExplorerMatched:
|
||||
comments["module_name"] = "metrics-explorer"
|
||||
case logsExplorerMatched:
|
||||
comments["module_name"] = "logs-explorer"
|
||||
case traceExplorerMatched:
|
||||
comments["module_name"] = "traces-explorer"
|
||||
default:
|
||||
return comments
|
||||
}
|
||||
|
||||
if dashboardMatched {
|
||||
if matches := dashboardIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
comments["dashboard_id"] = matches[1]
|
||||
}
|
||||
|
||||
if matches := widgetIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
comments["widget_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
if ruleMatched {
|
||||
if matches := ruleIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
comments["rule_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
comments["http_path"] = referrerURL.Path
|
||||
|
||||
return comments
|
||||
}
|
||||
|
||||
// NewComment creates a new Comment with an empty map. It is safe to use concurrently.
|
||||
func NewComment() *Comment {
|
||||
return &Comment{vals: map[string]string{}}
|
||||
}
|
||||
|
||||
func (comment *Comment) Set(key, value string) {
|
||||
comment.mtx.Lock()
|
||||
defer comment.mtx.Unlock()
|
||||
|
||||
comment.vals[key] = value
|
||||
}
|
||||
|
||||
func (comment *Comment) Merge(vals map[string]string) {
|
||||
comment.mtx.Lock()
|
||||
defer comment.mtx.Unlock()
|
||||
|
||||
// If vals is nil, do nothing. Comment should not panic.
|
||||
if vals == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for key, value := range vals {
|
||||
comment.vals[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func (comment *Comment) Map() map[string]string {
|
||||
comment.mtx.RLock()
|
||||
defer comment.mtx.RUnlock()
|
||||
|
||||
copyOfVals := make(map[string]string)
|
||||
for key, value := range comment.vals {
|
||||
copyOfVals[key] = value
|
||||
}
|
||||
|
||||
return copyOfVals
|
||||
}
|
||||
|
||||
func (comment *Comment) String() string {
|
||||
comment.mtx.RLock()
|
||||
defer comment.mtx.RUnlock()
|
||||
|
||||
commentJSON, err := json.Marshal(comment.vals)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
|
||||
return string(commentJSON)
|
||||
}
|
||||
|
||||
func (comment *Comment) Equal(other *Comment) bool {
|
||||
if len(comment.vals) != len(other.vals) {
|
||||
return false
|
||||
}
|
||||
|
||||
for key, value := range comment.vals {
|
||||
if val, ok := other.vals[key]; !ok || val != value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
123
pkg/types/ctxtypes/comment_test.go
Normal file
123
pkg/types/ctxtypes/comment_test.go
Normal file
@ -0,0 +1,123 @@
|
||||
package ctxtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommentFromHTTPRequest(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
req *http.Request
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "EmptyReferer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {""}}},
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "ControlCharacterInReferer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/logs/logs-explorer\x00"}}},
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "LogsExplorer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/logs/logs-explorer"}}},
|
||||
expected: map[string]string{"http_path": "/logs/logs-explorer", "module_name": "logs-explorer"},
|
||||
},
|
||||
{
|
||||
name: "TracesExplorer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/traces-explorer"}}},
|
||||
expected: map[string]string{"http_path": "/traces-explorer", "module_name": "traces-explorer"},
|
||||
},
|
||||
{
|
||||
name: "MetricsExplorer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/metrics-explorer/explorer"}}},
|
||||
expected: map[string]string{"http_path": "/metrics-explorer/explorer", "module_name": "metrics-explorer"},
|
||||
},
|
||||
{
|
||||
name: "DashboardWithID",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/dashboard/123/new"}}},
|
||||
expected: map[string]string{"http_path": "/dashboard/123/new", "module_name": "dashboard", "dashboard_id": "123"},
|
||||
},
|
||||
{
|
||||
name: "Rule",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/alerts/new"}}},
|
||||
expected: map[string]string{"http_path": "/alerts/new", "module_name": "rule"},
|
||||
},
|
||||
{
|
||||
name: "RuleWithID",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/alerts/edit?ruleId=123"}}},
|
||||
expected: map[string]string{"http_path": "/alerts/edit", "module_name": "rule", "rule_id": "123"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := CommentFromHTTPRequest(tc.req)
|
||||
|
||||
assert.True(t, (&Comment{vals: tc.expected}).Equal(&Comment{vals: actual}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentFromContext(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
comment1 := CommentFromContext(ctx)
|
||||
assert.True(t, NewComment().Equal(comment1))
|
||||
|
||||
comment1.Set("k1", "v1")
|
||||
ctx = NewContextWithComment(ctx, comment1)
|
||||
actual1 := CommentFromContext(ctx)
|
||||
assert.True(t, comment1.Equal(actual1))
|
||||
|
||||
// Get the comment from the context, mutate it, but this time do not set it back in the context
|
||||
comment2 := CommentFromContext(ctx)
|
||||
comment2.Set("k2", "v2")
|
||||
|
||||
actual2 := CommentFromContext(ctx)
|
||||
// Since comment2 was not set back in the context, it should not affect the original comment1
|
||||
assert.True(t, comment1.Equal(actual2))
|
||||
assert.False(t, comment2.Equal(actual2))
|
||||
assert.False(t, comment1.Equal(comment2))
|
||||
}
|
||||
|
||||
func TestCommentFromContextConcurrent(t *testing.T) {
|
||||
comment := NewComment()
|
||||
comment.Set("k1", "v1")
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = NewContextWithComment(ctx, comment)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
ctxs := make([]context.Context, 10)
|
||||
var mtx sync.Mutex
|
||||
wg.Add(10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
comment := CommentFromContext(ctx)
|
||||
comment.Set("k2", fmt.Sprintf("v%d", i))
|
||||
newCtx := NewContextWithComment(ctx, comment)
|
||||
mtx.Lock()
|
||||
ctxs[i] = newCtx
|
||||
mtx.Unlock()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for i, ctx := range ctxs {
|
||||
comment := CommentFromContext(ctx)
|
||||
assert.Equal(t, len(comment.vals), 2)
|
||||
assert.Equal(t, comment.vals["k1"], "v1")
|
||||
assert.Equal(t, comment.vals["k2"], fmt.Sprintf("v%d", i))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user